Merge branch 'main' into #3-calendar
# Conflicts: # package-lock.json # src/main.css # src/router/authGuards.ts # src/types/permissionTypes.ts
This commit is contained in:
commit
8074dbf5c4
38 changed files with 2228 additions and 66 deletions
192
package-lock.json
generated
192
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "fireportal-ui",
|
"name": "fireportal-ui",
|
||||||
"version": "0.0.2",
|
"version": "0.0.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "fireportal-ui",
|
"name": "fireportal-ui",
|
||||||
"version": "0.0.2",
|
"version": "0.0.3",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fullcalendar/core": "^6.1.15",
|
"@fullcalendar/core": "^6.1.15",
|
||||||
|
@ -16,9 +16,12 @@
|
||||||
"@fullcalendar/vue3": "^6.1.15",
|
"@fullcalendar/vue3": "^6.1.15",
|
||||||
"@headlessui/vue": "^1.7.13",
|
"@headlessui/vue": "^1.7.13",
|
||||||
"@heroicons/vue": "^2.1.5",
|
"@heroicons/vue": "^2.1.5",
|
||||||
|
"@vueup/vue-quill": "^1.2.0",
|
||||||
"axios": "^0.26.1",
|
"axios": "^0.26.1",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"lodash.difference": "^4.5.0",
|
||||||
|
"lodash.differencewith": "^4.5.0",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pdf-dist": "^1.0.0",
|
"pdf-dist": "^1.0.0",
|
||||||
|
@ -35,6 +38,8 @@
|
||||||
"@tsconfig/node20": "^20.1.4",
|
"@tsconfig/node20": "^20.1.4",
|
||||||
"@types/eslint": "~9.6.0",
|
"@types/eslint": "~9.6.0",
|
||||||
"@types/lodash.clonedeep": "^4.5.9",
|
"@types/lodash.clonedeep": "^4.5.9",
|
||||||
|
"@types/lodash.difference": "^4.5.9",
|
||||||
|
"@types/lodash.differencewith": "^4.5.9",
|
||||||
"@types/lodash.isequal": "^4.5.8",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
"@types/node": "^20.14.5",
|
"@types/node": "^20.14.5",
|
||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
|
@ -3137,6 +3142,26 @@
|
||||||
"@types/lodash": "*"
|
"@types/lodash": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/lodash.difference": {
|
||||||
|
"version": "4.5.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash.difference/-/lodash.difference-4.5.9.tgz",
|
||||||
|
"integrity": "sha512-MNlajcjtwzLpXk+cw38UkBvEXJNEPhULgS8A4EHwtUwT7f7yFH/SFKD0iw5Rfilwh60yJIgFo0vsMr7xsa5+aw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/lodash": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/lodash.differencewith": {
|
||||||
|
"version": "4.5.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash.differencewith/-/lodash.differencewith-4.5.9.tgz",
|
||||||
|
"integrity": "sha512-nMaREKoe7J3WvnsO7HDRxvnPT3mWmZD3EAECpy7gBGJ6S5nQ66uVlkRe+ZXs6261ZNb2fH9Ny4oUUiSOCmTnLw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/lodash": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/lodash.isequal": {
|
"node_modules/@types/lodash.isequal": {
|
||||||
"version": "4.5.8",
|
"version": "4.5.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.8.tgz",
|
||||||
|
@ -3736,6 +3761,19 @@
|
||||||
"integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==",
|
"integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@vueup/vue-quill": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueup/vue-quill/-/vue-quill-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-kd5QPSHMDpycklojPXno2Kw2JSiKMYduKYQckTm1RJoVDA557MnyUXgcuuDpry4HY/Rny9nGNcK+m3AHk94wag==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"quill": "^1.3.7",
|
||||||
|
"quill-delta": "^4.2.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.2.41"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.12.1",
|
"version": "8.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||||
|
@ -4367,6 +4405,15 @@
|
||||||
"wrap-ansi": "^6.2.0"
|
"wrap-ansi": "^6.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/clone": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color": {
|
"node_modules/color": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||||
|
@ -4645,6 +4692,26 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/deep-equal": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-arguments": "^1.1.1",
|
||||||
|
"is-date-object": "^1.0.5",
|
||||||
|
"is-regex": "^1.1.4",
|
||||||
|
"object-is": "^1.1.5",
|
||||||
|
"object-keys": "^1.1.1",
|
||||||
|
"regexp.prototype.flags": "^1.5.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/deep-extend": {
|
"node_modules/deep-extend": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||||
|
@ -4729,7 +4796,6 @@
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
|
||||||
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"define-data-property": "^1.0.1",
|
"define-data-property": "^1.0.1",
|
||||||
"has-property-descriptors": "^1.0.0",
|
"has-property-descriptors": "^1.0.0",
|
||||||
|
@ -5293,6 +5359,12 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/execa": {
|
"node_modules/execa": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
|
||||||
|
@ -5325,6 +5397,12 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/extend": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
@ -5606,7 +5684,6 @@
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
|
||||||
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
|
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
|
||||||
"dev": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
|
@ -5871,7 +5948,6 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-symbols": "^1.0.3"
|
"has-symbols": "^1.0.3"
|
||||||
},
|
},
|
||||||
|
@ -6032,6 +6108,22 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-arguments": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.2",
|
||||||
|
"has-tostringtag": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
|
||||||
|
@ -6155,7 +6247,6 @@
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
|
||||||
"integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
|
"integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-tostringtag": "^1.0.0"
|
"has-tostringtag": "^1.0.0"
|
||||||
},
|
},
|
||||||
|
@ -6292,7 +6383,6 @@
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
|
||||||
"integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
|
"integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind": "^1.0.2",
|
"call-bind": "^1.0.2",
|
||||||
"has-tostringtag": "^1.0.0"
|
"has-tostringtag": "^1.0.0"
|
||||||
|
@ -6690,6 +6780,18 @@
|
||||||
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.difference": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.differencewith": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.differencewith/-/lodash.differencewith-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-/8JFjydAS+4bQuo3CpLMBv7WxGFyk7/etOAsrQUCu0a9QVDemxv0YQ0rFyeZvqlUD314SERfNlgnlqqHmaQ0Cg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.isequal": {
|
"node_modules/lodash.isequal": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||||
|
@ -7052,11 +7154,26 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-is": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.7",
|
||||||
|
"define-properties": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-keys": {
|
"node_modules/object-keys": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
|
@ -7182,6 +7299,12 @@
|
||||||
"integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==",
|
"integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/parchment": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/parent-module": {
|
"node_modules/parent-module": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||||
|
@ -7735,6 +7858,57 @@
|
||||||
"integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
|
"integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/quill": {
|
||||||
|
"version": "1.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz",
|
||||||
|
"integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"clone": "^2.1.1",
|
||||||
|
"deep-equal": "^1.0.1",
|
||||||
|
"eventemitter3": "^2.0.3",
|
||||||
|
"extend": "^3.0.2",
|
||||||
|
"parchment": "^1.1.4",
|
||||||
|
"quill-delta": "^3.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/quill-delta": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-qjbn82b/yJzOjstBgkhtBjN2TNK+ZHP/BgUQO+j6bRhWQQdmj2lH6hXG7+nwwLF41Xgn//7/83lxs9n2BkTtTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-diff": "1.2.0",
|
||||||
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"lodash.isequal": "^4.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/quill-delta/node_modules/fast-diff": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/quill/node_modules/fast-diff": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/quill/node_modules/quill-delta": {
|
||||||
|
"version": "3.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
|
||||||
|
"integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"deep-equal": "^1.0.1",
|
||||||
|
"extend": "^3.0.2",
|
||||||
|
"fast-diff": "1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/randombytes": {
|
"node_modules/randombytes": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||||
|
@ -7853,7 +8027,6 @@
|
||||||
"version": "1.5.2",
|
"version": "1.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
|
||||||
"integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==",
|
"integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind": "^1.0.6",
|
"call-bind": "^1.0.6",
|
||||||
"define-properties": "^1.2.1",
|
"define-properties": "^1.2.1",
|
||||||
|
@ -8156,7 +8329,6 @@
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
|
||||||
"integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
|
"integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"define-data-property": "^1.1.4",
|
"define-data-property": "^1.1.4",
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "fireportal-ui",
|
"name": "fireportal-ui",
|
||||||
"version": "0.0.2",
|
"version": "0.0.3",
|
||||||
"description": "Feuerwehr AlarmPortal UI",
|
"description": "Feuerwehr AlarmPortal UI",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -31,9 +31,12 @@
|
||||||
"@fullcalendar/vue3": "^6.1.15",
|
"@fullcalendar/vue3": "^6.1.15",
|
||||||
"@headlessui/vue": "^1.7.13",
|
"@headlessui/vue": "^1.7.13",
|
||||||
"@heroicons/vue": "^2.1.5",
|
"@heroicons/vue": "^2.1.5",
|
||||||
|
"@vueup/vue-quill": "^1.2.0",
|
||||||
"axios": "^0.26.1",
|
"axios": "^0.26.1",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"lodash.difference": "^4.5.0",
|
||||||
|
"lodash.differencewith": "^4.5.0",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pdf-dist": "^1.0.0",
|
"pdf-dist": "^1.0.0",
|
||||||
|
@ -50,6 +53,8 @@
|
||||||
"@tsconfig/node20": "^20.1.4",
|
"@tsconfig/node20": "^20.1.4",
|
||||||
"@types/eslint": "~9.6.0",
|
"@types/eslint": "~9.6.0",
|
||||||
"@types/lodash.clonedeep": "^4.5.9",
|
"@types/lodash.clonedeep": "^4.5.9",
|
||||||
|
"@types/lodash.difference": "^4.5.9",
|
||||||
|
"@types/lodash.differencewith": "^4.5.9",
|
||||||
"@types/lodash.isequal": "^4.5.8",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
"@types/node": "^20.14.5",
|
"@types/node": "^20.14.5",
|
||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<header class="flex flex-row h-16 justify-between p-3 md:px-5 bg-white shadow-sm">
|
<header class="flex flex-row h-16 min-h-16 justify-between p-3 md:px-5 bg-white shadow-sm">
|
||||||
<RouterLink to="/" class="flex flex-row gap-2 align-bottom w-fit h-full">
|
<RouterLink to="/" class="flex flex-row gap-2 align-bottom w-fit h-full">
|
||||||
<img src="/FFW-Logo.svg" alt="LOGO" class="h-full w-auto" />
|
<img src="/FFW-Logo.svg" alt="LOGO" class="h-full w-auto" />
|
||||||
<h1 v-if="false" class="font-bold text-3xl w-fit whitespace-nowrap">Mitgliederverwaltung</h1>
|
<h1 v-if="false" class="font-bold text-3xl w-fit whitespace-nowrap">Mitgliederverwaltung</h1>
|
||||||
|
|
246
src/components/Pagination.vue
Normal file
246
src/components/Pagination.vue
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
<template>
|
||||||
|
<div class="grow flex flex-col gap-2 overflow-hidden">
|
||||||
|
<div v-if="useSearch" class="relative self-end">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="w-64 rounded-md shadow-sm relative block px-3 py-2 pr-5 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="Suche"
|
||||||
|
v-model="searchString"
|
||||||
|
/>
|
||||||
|
<XMarkIcon
|
||||||
|
class="absolute h-4 stroke-2 right-2 top-1/2 -translate-y-1/2 cursor-pointer z-10"
|
||||||
|
@click="searchString = ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col w-full grow gap-2 pr-2 overflow-y-scroll">
|
||||||
|
<div v-if="indicateLoading" class="flex flex-row justify-center items-center w-full p-1">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
<p v-if="visibleRows.length == 0" class="flex flex-row w-full gap-2 p-1">Kein Inhalt</p>
|
||||||
|
<slot
|
||||||
|
v-else
|
||||||
|
name="pageRow"
|
||||||
|
v-for="(item, index) in items"
|
||||||
|
:key="index"
|
||||||
|
:row="item"
|
||||||
|
@click="$emit('clickRow', item.id)"
|
||||||
|
>
|
||||||
|
<p>{{ item }}</p>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row w-full justify-between select-none">
|
||||||
|
<p class="text-sm font-normal text-gray-500">
|
||||||
|
Elemente <span class="font-semibold text-gray-900">{{ showingText }}</span> von
|
||||||
|
<span class="font-semibold text-gray-900">{{ entryCount }}</span>
|
||||||
|
</p>
|
||||||
|
<ul class="flex flex-row text-sm h-8">
|
||||||
|
<li
|
||||||
|
class="flex h-8 w-8 items-center justify-center text-gray-500 bg-white border border-gray-300 first:rounded-s-lg last:rounded-e-lg"
|
||||||
|
:class="[currentPage > 0 ? 'cursor-pointer hover:bg-gray-100 hover:text-gray-700' : 'opacity-50']"
|
||||||
|
@click="loadPage(currentPage - 1)"
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon class="h-4" />
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-for="page in displayedPagesNumbers"
|
||||||
|
:key="page"
|
||||||
|
class="flex h-8 w-8 items-center justify-center text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 first:rounded-s-lg last:rounded-e-lg"
|
||||||
|
:class="[currentPage == page ? 'font-bold border-primary' : '', page != '.' ? ' cursor-pointer' : '']"
|
||||||
|
@click="loadPage(page)"
|
||||||
|
>
|
||||||
|
{{ typeof page == "number" ? page + 1 : "..." }}
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="flex h-8 w-8 items-center justify-center text-gray-500 bg-white border border-gray-300 first:rounded-s-lg last:rounded-e-lg"
|
||||||
|
:class="[
|
||||||
|
currentPage + 1 < countOfPages ? 'cursor-pointer hover:bg-gray-100 hover:text-gray-700' : 'opacity-50',
|
||||||
|
]"
|
||||||
|
@click="loadPage(currentPage + 1)"
|
||||||
|
>
|
||||||
|
<ChevronRightIcon class="h-4" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" generic="T">
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { ChevronRightIcon, ChevronLeftIcon, XMarkIcon } from "@heroicons/vue/20/solid";
|
||||||
|
import Spinner from "./Spinner.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
items: { type: Array<T>, default: [] },
|
||||||
|
maxEntriesPerPage: { type: Number, default: 25 },
|
||||||
|
totalCount: { type: Number, default: null },
|
||||||
|
config: { type: Array<{ key: string }>, default: [] },
|
||||||
|
useSearch: { type: Boolean, default: false },
|
||||||
|
enablePreSearch: { type: Boolean, default: false },
|
||||||
|
indicateLoading: { type: Boolean, default: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const slots = defineSlots<{
|
||||||
|
pageRow(props: { row: T }): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const timer = ref(undefined) as undefined | any;
|
||||||
|
const currentPage = ref(0);
|
||||||
|
const searchString = ref("");
|
||||||
|
|
||||||
|
watch(searchString, async () => {
|
||||||
|
clearTimeout(timer.value);
|
||||||
|
timer.value = setTimeout(() => {
|
||||||
|
currentPage.value = 0;
|
||||||
|
emit("search", searchString.value);
|
||||||
|
}, 600);
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits({
|
||||||
|
submit(id: number) {
|
||||||
|
return typeof id == "number";
|
||||||
|
},
|
||||||
|
loadData(offset: number, count: number, searchString: string) {
|
||||||
|
return typeof offset == "number" && typeof offset == "number" && typeof searchString == "number";
|
||||||
|
},
|
||||||
|
search(search: string) {
|
||||||
|
return typeof search == "number";
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const entryCount = computed(() => props.totalCount ?? props.items.length);
|
||||||
|
const showingStart = computed(() => currentPage.value * props.maxEntriesPerPage);
|
||||||
|
const showingEnd = computed(() => {
|
||||||
|
let max = currentPage.value * props.maxEntriesPerPage + props.maxEntriesPerPage;
|
||||||
|
if (max > entryCount.value) max = entryCount.value;
|
||||||
|
return max;
|
||||||
|
});
|
||||||
|
const showingText = computed(() => `${entryCount.value != 0 ? showingStart.value + 1 : 0} - ${showingEnd.value}`);
|
||||||
|
const countOfPages = computed(() => Math.ceil(entryCount.value / props.maxEntriesPerPage));
|
||||||
|
const displayedPagesNumbers = computed(() => {
|
||||||
|
let stateOfPush = false;
|
||||||
|
|
||||||
|
return [...new Array(countOfPages.value)].reduce((acc, curr, index) => {
|
||||||
|
if (
|
||||||
|
index <= 1 ||
|
||||||
|
index >= countOfPages.value - 2 ||
|
||||||
|
(currentPage.value - 1 <= index && index <= currentPage.value + 1)
|
||||||
|
) {
|
||||||
|
acc.push(index);
|
||||||
|
stateOfPush = false;
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
if (stateOfPush == true) return acc;
|
||||||
|
acc.push(".");
|
||||||
|
stateOfPush = true;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
});
|
||||||
|
const visibleRows = computed(() => filterData(props.items, searchString.value, showingStart.value, showingEnd.value));
|
||||||
|
|
||||||
|
const loadPage = (newPage: number | ".") => {
|
||||||
|
if (newPage == ".") return;
|
||||||
|
if (newPage < 0 || newPage >= countOfPages.value) return;
|
||||||
|
|
||||||
|
let pageStart = newPage * props.maxEntriesPerPage;
|
||||||
|
let pageEnd = newPage * props.maxEntriesPerPage + props.maxEntriesPerPage;
|
||||||
|
if (pageEnd > entryCount.value) pageEnd = entryCount.value;
|
||||||
|
|
||||||
|
let loadedElementCount = filterData(props.items, searchString.value, pageStart, pageEnd).length;
|
||||||
|
if (loadedElementCount < props.maxEntriesPerPage)
|
||||||
|
emit("loadData", pageStart, props.maxEntriesPerPage, searchString.value);
|
||||||
|
|
||||||
|
currentPage.value = newPage;
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterData = (array: Array<any>, searchString: string, start: number, end: number): Array<any> => {
|
||||||
|
return array
|
||||||
|
.filter(
|
||||||
|
(elem) =>
|
||||||
|
!props.enablePreSearch ||
|
||||||
|
searchString.trim() == "" ||
|
||||||
|
props.config.some((col) => typeof elem?.[col.key] == "string" && elem[col.key].includes(searchString.trim()))
|
||||||
|
)
|
||||||
|
.filter((elem, index) => (elem?.tab_pos ?? index) >= start && (elem?.tab_pos ?? index) < end);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
computed: {
|
||||||
|
entryCount() {
|
||||||
|
return this.totalCount ?? this.items.length;
|
||||||
|
},
|
||||||
|
showingStart() {
|
||||||
|
return this.currentPage * this.maxEntriesPerPage;
|
||||||
|
},
|
||||||
|
showingEnd() {
|
||||||
|
let max = this.currentPage * this.maxEntriesPerPage + this.maxEntriesPerPage;
|
||||||
|
if (max > this.entryCount) max = this.entryCount;
|
||||||
|
return max;
|
||||||
|
},
|
||||||
|
showingText() {
|
||||||
|
return `${this.entryCount != 0 ? this.showingStart + 1 : 0} - ${this.showingEnd}`;
|
||||||
|
},
|
||||||
|
countOfPages() {
|
||||||
|
return Math.ceil(this.entryCount / this.maxEntriesPerPage);
|
||||||
|
},
|
||||||
|
displayedPagesNumbers(): Array<number | "."> {
|
||||||
|
//indicate if "." or page number gets pushed
|
||||||
|
let stateOfPush = false;
|
||||||
|
|
||||||
|
return [...new Array(this.countOfPages)].reduce((acc, curr, index) => {
|
||||||
|
if (
|
||||||
|
// always display first 2 pages
|
||||||
|
index <= 1 ||
|
||||||
|
// always display last 2 pages
|
||||||
|
index >= this.countOfPages - 2 ||
|
||||||
|
// always display 1 pages around current page
|
||||||
|
(this.currentPage - 1 <= index && index <= this.currentPage + 1)
|
||||||
|
) {
|
||||||
|
acc.push(index);
|
||||||
|
stateOfPush = false;
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
// abort if placeholder already added to array
|
||||||
|
if (stateOfPush == true) return acc;
|
||||||
|
// show placeholder if pagenumber is not actively rendered
|
||||||
|
acc.push(".");
|
||||||
|
stateOfPush = true;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
},
|
||||||
|
visibleRows() {
|
||||||
|
return this.filterData(this.items, this.searchString, this.showingStart, this.showingEnd);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loadPage(newPage: number | ".") {
|
||||||
|
if (newPage == ".") return;
|
||||||
|
if (newPage < 0 || newPage >= this.countOfPages) return;
|
||||||
|
|
||||||
|
let pageStart = newPage * this.maxEntriesPerPage;
|
||||||
|
let pageEnd = newPage * this.maxEntriesPerPage + this.maxEntriesPerPage;
|
||||||
|
if (pageEnd > this.entryCount) pageEnd = this.entryCount;
|
||||||
|
|
||||||
|
let loadedElementCount = this.filterData(this.items, this.searchString, pageStart, pageEnd).length;
|
||||||
|
if (loadedElementCount < this.maxEntriesPerPage)
|
||||||
|
this.$emit("loadData", { offset: pageStart, count: this.maxEntriesPerPage, search: this.searchString });
|
||||||
|
|
||||||
|
this.currentPage = newPage;
|
||||||
|
},
|
||||||
|
filterData(array: Array<any>, searchString: string, start: number, end: number): Array<any> {
|
||||||
|
return array
|
||||||
|
.filter(
|
||||||
|
(elem) =>
|
||||||
|
!this.enablePreSearch ||
|
||||||
|
searchString.trim() == "" ||
|
||||||
|
this.config.some((col) => typeof elem?.[col.key] == "string" && elem[col.key].includes(searchString.trim()))
|
||||||
|
)
|
||||||
|
.filter((elem, index) => (elem?.tab_pos ?? index) >= start && (elem?.tab_pos ?? index) < end);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script> -->
|
81
src/components/admin/club/protocol/CreateProtocolModal.vue
Normal file
81
src/components/admin/club/protocol/CreateProtocolModal.vue
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full md:max-w-md">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<p class="text-xl font-medium">Protokoll erstellen</p>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<form ref="form" class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
|
||||||
|
<div>
|
||||||
|
<label for="title">Titel</label>
|
||||||
|
<input type="text" id="title" required autocomplete="false" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="date">Datum</label>
|
||||||
|
<input type="date" id="date" required />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
|
||||||
|
<Spinner v-if="status == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
||||||
|
<FailureXMark v-else-if="status?.status == 'failed'" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-end">
|
||||||
|
<div class="flex flex-row gap-4 py-2">
|
||||||
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import { useProtocolStore } from "@/stores/admin/protocol";
|
||||||
|
import type { CreateProtocolViewModel } from "@/viewmodels/admin/protocol.models";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||||
|
timeout: undefined as any,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
try {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
} catch (error) {}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
|
...mapActions(useProtocolStore, ["createProtocol"]),
|
||||||
|
triggerCreate(e: any) {
|
||||||
|
let formData = e.target.elements;
|
||||||
|
let createProtocol: CreateProtocolViewModel = {
|
||||||
|
title: formData.title.value,
|
||||||
|
date: formData.date.value,
|
||||||
|
};
|
||||||
|
this.createProtocol(createProtocol)
|
||||||
|
.then(() => {
|
||||||
|
this.status = { status: "success" };
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
(this.$refs.form as HTMLFormElement).reset();
|
||||||
|
this.closeModal();
|
||||||
|
}, 1500);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.status = { status: "failed" };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
26
src/components/admin/club/protocol/CurrentlySyncingModal.vue
Normal file
26
src/components/admin/club/protocol/CurrentlySyncingModal.vue
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full md:max-w-md">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<p class="text-xl font-medium">Protokoll wird noch synchronisiert</p>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<p>Es gibt noch Daten, welche synchronisiert werden müssen.</p>
|
||||||
|
<p>Dieses PopUp entfernt sich von selbst nach erfolgreicher Synchronisierung.</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import { useProtocolStore } from "@/stores/admin/protocol";
|
||||||
|
import type { CreateProtocolViewModel } from "@/viewmodels/admin/protocol.models";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({});
|
||||||
|
</script>
|
27
src/components/admin/club/protocol/ProtocolListItem.vue
Normal file
27
src/components/admin/club/protocol/ProtocolListItem.vue
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
|
||||||
|
<RouterLink
|
||||||
|
:to="{ name: 'admin-club-protocol-overview', params: { protocolId: protocol.id } }"
|
||||||
|
class="bg-primary p-2 text-white flex flex-row justify-between items-center"
|
||||||
|
>
|
||||||
|
<p>{{ protocol.title }} - {{ protocol.date }}</p>
|
||||||
|
</RouterLink>
|
||||||
|
<div class="p-2 max-h-48 overflow-y-auto">
|
||||||
|
<p v-html="protocol.summary"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import type { ProtocolViewModel } from "../../../../viewmodels/admin/protocol.models";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
protocol: { type: Object as PropType<ProtocolViewModel>, default: {} },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,40 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full flex flex-col gap-2">
|
||||||
|
<div class="grow">
|
||||||
|
<iframe ref="viewer" class="w-full h-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button primary-outline class="!w-fit self-end" @click="closeModal">schließen</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import { useProtocolPrintoutStore } from "@/stores/admin/protocolPrintout";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
computed: {
|
||||||
|
...mapState(useModalStore, ["data"]),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchItem();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
|
...mapActions(useProtocolPrintoutStore, ["fetchProtocolPrintoutById"]),
|
||||||
|
fetchItem() {
|
||||||
|
this.fetchProtocolPrintoutById(this.data)
|
||||||
|
.then((response) => {
|
||||||
|
const blob = new Blob([response.data], { type: "application/pdf" });
|
||||||
|
(this.$refs.viewer as HTMLIFrameElement).src = window.URL.createObjectURL(blob);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
161
src/components/admin/club/protocol/ProtocolSyncing.vue
Normal file
161
src/components/admin/club/protocol/ProtocolSyncing.vue
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
<template>
|
||||||
|
<CloudIcon v-if="syncing == 'synced'" class="w-5 h-5" />
|
||||||
|
<CloudArrowUpIcon
|
||||||
|
v-else-if="syncing == 'detectedChanges'"
|
||||||
|
class="w-5 h-5 cursor-pointer animate-bounce"
|
||||||
|
@click="syncAll"
|
||||||
|
/>
|
||||||
|
<ArrowPathIcon v-else-if="syncing == 'syncing'" class="w-5 h-5 animate-spin" />
|
||||||
|
<ExclamationTriangleIcon
|
||||||
|
v-else
|
||||||
|
class="w-5 h-5 animate-[ping_1s_ease-in-out_3] text-red-500 cursor-pointer"
|
||||||
|
@click="syncAll"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useProtocolStore } from "@/stores/admin/protocol";
|
||||||
|
import { ArrowPathIcon, CloudArrowUpIcon, CloudIcon, ExclamationTriangleIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import { useProtocolAgendaStore } from "@/stores/admin/protocolAgenda";
|
||||||
|
import { useProtocolPresenceStore } from "@/stores/admin/protocolPresence";
|
||||||
|
import { useProtocolDecisionStore } from "../../../../stores/admin/protocolDecision";
|
||||||
|
import { useProtocolVotingStore } from "../../../../stores/admin/protocolVoting";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: ["executeSyncAll"],
|
||||||
|
watch: {
|
||||||
|
executeSyncAll() {
|
||||||
|
this.syncAll();
|
||||||
|
},
|
||||||
|
syncing() {
|
||||||
|
this.$emit("syncState", this.syncing);
|
||||||
|
},
|
||||||
|
detectedChangeProtocol() {
|
||||||
|
clearTimeout(this.protocolTimer);
|
||||||
|
this.setProtocolSyncingState("synced");
|
||||||
|
if (this.detectedChangeProtocol == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setProtocolSyncingState("detectedChanges");
|
||||||
|
this.protocolTimer = setTimeout(() => {
|
||||||
|
this.synchronizeActiveProtocol();
|
||||||
|
}, 10000);
|
||||||
|
},
|
||||||
|
detectedChangeProtocolAgenda() {
|
||||||
|
clearTimeout(this.protocolAgendaTimer);
|
||||||
|
if (this.detectedChangeProtocolAgenda == false) {
|
||||||
|
this.setProtocolAgendaSyncingState("synced");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setProtocolAgendaSyncingState("detectedChanges");
|
||||||
|
this.protocolAgendaTimer = setTimeout(() => {
|
||||||
|
this.synchronizeActiveProtocolAgenda();
|
||||||
|
}, 10000);
|
||||||
|
},
|
||||||
|
detectedChangeProtocolPresence() {
|
||||||
|
clearTimeout(this.protocolPresenceTimer);
|
||||||
|
this.setProtocolPresenceSyncingState("synced");
|
||||||
|
if (this.detectedChangeProtocolPresence == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setProtocolPresenceSyncingState("detectedChanges");
|
||||||
|
this.protocolPresenceTimer = setTimeout(() => {
|
||||||
|
this.synchronizeActiveProtocolPresence();
|
||||||
|
}, 10000);
|
||||||
|
},
|
||||||
|
detectedChangeProtocolDecision() {
|
||||||
|
clearTimeout(this.protocolDecisionTimer);
|
||||||
|
this.setProtocolDecisionSyncingState("synced");
|
||||||
|
if (this.detectedChangeProtocolDecision == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setProtocolDecisionSyncingState("detectedChanges");
|
||||||
|
this.protocolDecisionTimer = setTimeout(() => {
|
||||||
|
this.synchronizeActiveProtocolDecision();
|
||||||
|
}, 10000);
|
||||||
|
},
|
||||||
|
detectedChangeProtocolVoting() {
|
||||||
|
clearTimeout(this.protocolVotingTimer);
|
||||||
|
this.setProtocolVotingSyncingState("synced");
|
||||||
|
if (this.detectedChangeProtocolVoting == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setProtocolVotingSyncingState("detectedChanges");
|
||||||
|
this.protocolVotingTimer = setTimeout(() => {
|
||||||
|
this.synchronizeActiveProtocolVoting();
|
||||||
|
}, 10000);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: {
|
||||||
|
syncState(state: "synced" | "syncing" | "detectedChanges" | "failed") {
|
||||||
|
return typeof state == "string";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
protocolTimer: undefined as undefined | any,
|
||||||
|
protocolAgendaTimer: undefined as undefined | any,
|
||||||
|
protocolPresenceTimer: undefined as undefined | any,
|
||||||
|
protocolDecisionTimer: undefined as undefined | any,
|
||||||
|
protocolVotingTimer: undefined as undefined | any,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$emit("syncState", this.syncing);
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if (!this.protocolTimer) clearTimeout(this.protocolTimer);
|
||||||
|
if (!this.protocolAgendaTimer) clearTimeout(this.protocolAgendaTimer);
|
||||||
|
if (!this.protocolPresenceTimer) clearTimeout(this.protocolPresenceTimer);
|
||||||
|
if (!this.protocolDecisionTimer) clearTimeout(this.protocolDecisionTimer);
|
||||||
|
if (!this.protocolVotingTimer) clearTimeout(this.protocolVotingTimer);
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useProtocolStore, ["syncingProtocol", "detectedChangeProtocol"]),
|
||||||
|
...mapState(useProtocolAgendaStore, ["syncingProtocolAgenda", "detectedChangeProtocolAgenda"]),
|
||||||
|
...mapState(useProtocolPresenceStore, ["syncingProtocolPresence", "detectedChangeProtocolPresence"]),
|
||||||
|
...mapState(useProtocolDecisionStore, ["syncingProtocolDecision", "detectedChangeProtocolDecision"]),
|
||||||
|
...mapState(useProtocolVotingStore, ["syncingProtocolVoting", "detectedChangeProtocolVoting"]),
|
||||||
|
|
||||||
|
syncing(): "synced" | "syncing" | "detectedChanges" | "failed" {
|
||||||
|
let states = [
|
||||||
|
this.syncingProtocol,
|
||||||
|
this.syncingProtocolAgenda,
|
||||||
|
this.syncingProtocolPresence,
|
||||||
|
this.syncingProtocolDecision,
|
||||||
|
this.syncingProtocolVoting,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (states.includes("failed")) return "failed";
|
||||||
|
else if (states.includes("syncing")) return "syncing";
|
||||||
|
else if (states.includes("detectedChanges")) return "detectedChanges";
|
||||||
|
else return "synced";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useProtocolStore, ["synchronizeActiveProtocol", "setProtocolSyncingState"]),
|
||||||
|
...mapActions(useProtocolAgendaStore, ["synchronizeActiveProtocolAgenda", "setProtocolAgendaSyncingState"]),
|
||||||
|
...mapActions(useProtocolPresenceStore, ["synchronizeActiveProtocolPresence", "setProtocolPresenceSyncingState"]),
|
||||||
|
...mapActions(useProtocolDecisionStore, ["synchronizeActiveProtocolDecision", "setProtocolDecisionSyncingState"]),
|
||||||
|
...mapActions(useProtocolVotingStore, ["synchronizeActiveProtocolVoting", "setProtocolVotingSyncingState"]),
|
||||||
|
|
||||||
|
syncAll() {
|
||||||
|
if (!this.protocolTimer) clearTimeout(this.protocolTimer);
|
||||||
|
if (!this.protocolAgendaTimer) clearTimeout(this.protocolAgendaTimer);
|
||||||
|
if (!this.protocolPresenceTimer) clearTimeout(this.protocolPresenceTimer);
|
||||||
|
if (!this.protocolDecisionTimer) clearTimeout(this.protocolDecisionTimer);
|
||||||
|
if (!this.protocolVotingTimer) clearTimeout(this.protocolVotingTimer);
|
||||||
|
|
||||||
|
this.synchronizeActiveProtocol();
|
||||||
|
this.synchronizeActiveProtocolAgenda();
|
||||||
|
this.synchronizeActiveProtocolPresence();
|
||||||
|
this.synchronizeActiveProtocolDecision();
|
||||||
|
this.synchronizeActiveProtocolVoting();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -1,12 +1,10 @@
|
||||||
import type { AxiosInstance } from "axios";
|
import type { AxiosInstance } from "axios";
|
||||||
import type { NProgress } from "nprogress";
|
|
||||||
import type { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
import type { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||||
|
|
||||||
declare module "@vue/runtime-core" {
|
declare module "@vue/runtime-core" {
|
||||||
interface ComponentCustomProperties {
|
interface ComponentCustomProperties {
|
||||||
$dev: boolean;
|
$dev: boolean;
|
||||||
$http: AxiosInstance;
|
$http: AxiosInstance;
|
||||||
$progress: NProgress;
|
|
||||||
$router: Router;
|
$router: Router;
|
||||||
$route: RouteLocationNormalizedLoaded;
|
$route: RouteLocationNormalizedLoaded;
|
||||||
}
|
}
|
||||||
|
|
8
src/helpers/quillConfig.ts
Normal file
8
src/helpers/quillConfig.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export const toolbarOptions = [
|
||||||
|
[/*{ header: [1, 2, false] },*/ { font: [] }],
|
||||||
|
//[{ header: 1 }, { header: 2 }],
|
||||||
|
["bold", "italic", "underline", "strike"],
|
||||||
|
["blockquote", "code-block", "link"],
|
||||||
|
[{ list: "ordered" }, { list: "bullet" }, { list: "check" }],
|
||||||
|
["clean"],
|
||||||
|
];
|
31
src/main.css
31
src/main.css
|
@ -56,8 +56,8 @@ body {
|
||||||
@apply w-full h-full overflow-hidden flex flex-col;
|
@apply w-full h-full overflow-hidden flex flex-col;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:not([headlessui]):not([class*="fc"]),
|
button:not([headlessui]):not([id*="headlessui"]):not([class*="headlessui"]):not([class*="ql"] *):not([class*="fc"]),
|
||||||
a[button]:not([headlessui]) {
|
a[button] {
|
||||||
@apply relative box-border h-10 w-full flex justify-center py-2 px-4 text-sm font-medium rounded-md focus:outline-none focus:ring-0;
|
@apply relative box-border h-10 w-full flex justify-center py-2 px-4 text-sm font-medium rounded-md focus:outline-none focus:ring-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,6 +92,33 @@ textarea[disabled] {
|
||||||
@apply opacity-75 pointer-events-none;
|
@apply opacity-75 pointer-events-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
details {
|
||||||
|
user-select: none;
|
||||||
|
& summary svg {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
details[open] {
|
||||||
|
& summary svg {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open] summary ~ * {
|
||||||
|
animation: ease-opacity-t-b 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
summary > svg {
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.fc-button-primary {
|
.fc-button-primary {
|
||||||
@apply !bg-primary !border-primary !outline-none !ring-0 hover:!bg-red-700 hover:!border-red-700;
|
@apply !bg-primary !border-primary !outline-none !ring-0 hover:!bg-red-700 hover:!border-red-700;
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,7 +69,7 @@ export async function isAuthenticatedPromise(): Promise<Payload> {
|
||||||
|
|
||||||
var { firstname, lastname, mail, username, permissions, isOwner } = decoded;
|
var { firstname, lastname, mail, username, permissions, isOwner } = decoded;
|
||||||
|
|
||||||
if (Object.keys(permissions).length === 0 && !isOwner) {
|
if (Object.keys(permissions ?? {}).length === 0 && !isOwner) {
|
||||||
auth.setFailed();
|
auth.setFailed();
|
||||||
reject("nopermissions");
|
reject("nopermissions");
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { isSetup } from "./setupGuard";
|
||||||
import { abilityAndNavUpdate } from "./adminGuard";
|
import { abilityAndNavUpdate } from "./adminGuard";
|
||||||
import type { PermissionType, PermissionSection, PermissionModule } from "@/types/permissionTypes";
|
import type { PermissionType, PermissionSection, PermissionModule } from "@/types/permissionTypes";
|
||||||
import { resetMemberStores, setMemberId } from "./memberGuard";
|
import { resetMemberStores, setMemberId } from "./memberGuard";
|
||||||
|
import { resetProtocolStores, setProtocolId } from "./protocolGuard";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
@ -148,10 +149,63 @@ const router = createRouter({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "protocol",
|
path: "protocol",
|
||||||
name: "admin-club-protocol",
|
name: "admin-club-protocol-route",
|
||||||
component: () => import("@/views/admin/members/Overview.vue"),
|
component: () => import("@/views/RouterView.vue"),
|
||||||
meta: { type: "read", section: "club", module: "protocoll" },
|
meta: { type: "read", section: "club", module: "protocol" },
|
||||||
beforeEnter: [abilityAndNavUpdate],
|
beforeEnter: [abilityAndNavUpdate],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
name: "admin-club-protocol",
|
||||||
|
component: () => import("@/views/admin/club/protocol/Protocol.vue"),
|
||||||
|
beforeEnter: [resetProtocolStores],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ":protocolId",
|
||||||
|
name: "admin-club-protocol-routing",
|
||||||
|
component: () => import("@/views/admin/club/protocol/ProtocolRouting.vue"),
|
||||||
|
beforeEnter: [setProtocolId],
|
||||||
|
props: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "overview",
|
||||||
|
name: "admin-club-protocol-overview",
|
||||||
|
component: () => import("@/views/admin/club/protocol/ProtocolOverview.vue"),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "presence",
|
||||||
|
name: "admin-club-protocol-presence",
|
||||||
|
component: () => import("@/views/admin/club/protocol/ProtocolPresence.vue"),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "voting",
|
||||||
|
name: "admin-club-protocol-voting",
|
||||||
|
component: () => import("@/views/admin/club/protocol/ProtocolVoting.vue"),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "decisions",
|
||||||
|
name: "admin-club-protocol-decisions",
|
||||||
|
component: () => import("@/views/admin/club/protocol/ProtocolDecisions.vue"),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "agenda",
|
||||||
|
name: "admin-club-protocol-agenda",
|
||||||
|
component: () => import("@/views/admin/club/protocol/ProtocolAgenda.vue"),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "printout",
|
||||||
|
name: "admin-club-protocol-printout",
|
||||||
|
component: () => import("@/views/admin/club/protocol/ProtocolPrintout.vue"),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
33
src/router/protocolGuard.ts
Normal file
33
src/router/protocolGuard.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { useProtocolStore } from "@/stores/admin/protocol";
|
||||||
|
import { useProtocolAgendaStore } from "@/stores/admin/protocolAgenda";
|
||||||
|
import { useProtocolDecisionStore } from "@/stores/admin/protocolDecision";
|
||||||
|
import { useProtocolPresenceStore } from "@/stores/admin/protocolPresence";
|
||||||
|
import { useProtocolVotingStore } from "@/stores/admin/protocolVoting";
|
||||||
|
import { useProtocolPrintoutStore } from "../stores/admin/protocolPrintout";
|
||||||
|
|
||||||
|
export async function setProtocolId(to: any, from: any, next: any) {
|
||||||
|
const protocol = useProtocolStore();
|
||||||
|
protocol.activeProtocol = to.params?.protocolId ?? null;
|
||||||
|
|
||||||
|
useProtocolAgendaStore().$reset();
|
||||||
|
useProtocolDecisionStore().$reset();
|
||||||
|
useProtocolPresenceStore().$reset();
|
||||||
|
useProtocolVotingStore().$reset();
|
||||||
|
useProtocolPrintoutStore().$reset();
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resetProtocolStores(to: any, from: any, next: any) {
|
||||||
|
const protocol = useProtocolStore();
|
||||||
|
protocol.activeProtocol = null;
|
||||||
|
protocol.activeProtocolObj = null;
|
||||||
|
|
||||||
|
useProtocolAgendaStore().$reset();
|
||||||
|
useProtocolDecisionStore().$reset();
|
||||||
|
useProtocolPresenceStore().$reset();
|
||||||
|
useProtocolVotingStore().$reset();
|
||||||
|
useProtocolPrintoutStore().$reset();
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
105
src/stores/admin/protocol.ts
Normal file
105
src/stores/admin/protocol.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import type { CreateProtocolViewModel, SyncProtocolViewModel } from "@/viewmodels/admin/protocol.models";
|
||||||
|
import { http } from "@/serverCom";
|
||||||
|
import type { AxiosResponse } from "axios";
|
||||||
|
import type { ProtocolViewModel } from "@/viewmodels/admin/protocol.models";
|
||||||
|
import cloneDeep from "lodash.clonedeep";
|
||||||
|
import isEqual from "lodash.isEqual";
|
||||||
|
import difference from "lodash.difference";
|
||||||
|
|
||||||
|
export const useProtocolStore = defineStore("protocol", {
|
||||||
|
state: () => {
|
||||||
|
return {
|
||||||
|
protocols: [] as Array<ProtocolViewModel & { tab_pos: number }>,
|
||||||
|
totalCount: 0 as number,
|
||||||
|
loading: "loading" as "loading" | "fetched" | "failed",
|
||||||
|
activeProtocol: null as number | null,
|
||||||
|
activeProtocolObj: null as ProtocolViewModel | null,
|
||||||
|
origin: null as ProtocolViewModel | null,
|
||||||
|
loadingActive: "loading" as "loading" | "fetched" | "failed",
|
||||||
|
syncingProtocol: "synced" as "synced" | "syncing" | "detectedChanges" | "failed",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
detectedChangeProtocol: (state) =>
|
||||||
|
!isEqual(state.origin, state.activeProtocolObj) && state.syncingProtocol != "syncing",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
setProtocolSyncingState(state: "synced" | "syncing" | "detectedChanges" | "failed") {
|
||||||
|
this.syncingProtocol = state;
|
||||||
|
},
|
||||||
|
fetchProtocols(offset = 0, count = 25, clear = false) {
|
||||||
|
if (clear) this.protocols = [];
|
||||||
|
this.loading = "loading";
|
||||||
|
http
|
||||||
|
.get(`/admin/protocol?offset=${offset}&count=${count}`)
|
||||||
|
.then((result) => {
|
||||||
|
this.totalCount = result.data.total;
|
||||||
|
result.data.protocols
|
||||||
|
.filter((elem: ProtocolViewModel) => this.protocols.findIndex((m) => m.id == elem.id) == -1)
|
||||||
|
.map((elem: ProtocolViewModel, index: number): ProtocolViewModel & { tab_pos: number } => {
|
||||||
|
return {
|
||||||
|
...elem,
|
||||||
|
tab_pos: index + offset,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.forEach((elem: ProtocolViewModel & { tab_pos: number }) => {
|
||||||
|
this.protocols.push(elem);
|
||||||
|
});
|
||||||
|
this.loading = "fetched";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.loading = "failed";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fetchProtocolByActiveId() {
|
||||||
|
this.loadingActive = "loading";
|
||||||
|
http
|
||||||
|
.get(`/admin/protocol/${this.activeProtocol}`)
|
||||||
|
.then((res) => {
|
||||||
|
this.origin = res.data;
|
||||||
|
this.activeProtocolObj = cloneDeep(this.origin);
|
||||||
|
this.loadingActive = "fetched";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.loadingActive = "failed";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fetchProtocolById(id: number) {
|
||||||
|
return http.get(`/admin/protocol/${id}`);
|
||||||
|
},
|
||||||
|
async createProtocol(protocol: CreateProtocolViewModel): Promise<AxiosResponse<any, any>> {
|
||||||
|
const result = await http.post(`/admin/protocol`, {
|
||||||
|
title: protocol.title,
|
||||||
|
date: protocol.date,
|
||||||
|
});
|
||||||
|
this.fetchProtocols();
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
async synchronizeActiveProtocol(): Promise<void> {
|
||||||
|
if (this.origin == null || this.activeProtocolObj == null) return;
|
||||||
|
|
||||||
|
this.syncingProtocol = "syncing";
|
||||||
|
await http
|
||||||
|
.patch(`/admin/protocol/${this.origin.id}/synchronize`, {
|
||||||
|
title: this.activeProtocolObj.title,
|
||||||
|
date: this.activeProtocolObj.date,
|
||||||
|
starttime: this.activeProtocolObj.starttime,
|
||||||
|
endtime: this.activeProtocolObj.endtime,
|
||||||
|
summary: this.activeProtocolObj.summary,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
this.syncingProtocol = "synced";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.syncingProtocol = "failed";
|
||||||
|
});
|
||||||
|
this.fetchProtocolById(this.origin.id)
|
||||||
|
.then((res) => {
|
||||||
|
this.origin = res.data;
|
||||||
|
if (this.detectedChangeProtocol) this.syncingProtocol = "detectedChanges";
|
||||||
|
})
|
||||||
|
.catch((err) => {});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
82
src/stores/admin/protocolAgenda.ts
Normal file
82
src/stores/admin/protocolAgenda.ts
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { http } from "@/serverCom";
|
||||||
|
import type {
|
||||||
|
ProtocolAgendaViewModel,
|
||||||
|
SyncProtocolAgendaViewModel,
|
||||||
|
} from "../../viewmodels/admin/protocolAgenda.models";
|
||||||
|
import { useProtocolStore } from "./protocol";
|
||||||
|
import cloneDeep from "lodash.clonedeep";
|
||||||
|
import isEqual from "lodash.isEqual";
|
||||||
|
import differenceWith from "lodash.differencewith";
|
||||||
|
|
||||||
|
export const useProtocolAgendaStore = defineStore("protocolAgenda", {
|
||||||
|
state: () => {
|
||||||
|
return {
|
||||||
|
agenda: [] as Array<ProtocolAgendaViewModel>,
|
||||||
|
origin: [] as Array<ProtocolAgendaViewModel>,
|
||||||
|
loading: "loading" as "loading" | "fetched" | "failed",
|
||||||
|
syncingProtocolAgenda: "synced" as "synced" | "syncing" | "detectedChanges" | "failed",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
detectedChangeProtocolAgenda: (state) =>
|
||||||
|
!isEqual(state.origin, state.agenda) && state.syncingProtocolAgenda != "syncing",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
setProtocolAgendaSyncingState(state: "synced" | "syncing" | "detectedChanges" | "failed") {
|
||||||
|
this.syncingProtocolAgenda = state;
|
||||||
|
},
|
||||||
|
fetchProtocolAgenda() {
|
||||||
|
this.loading = "loading";
|
||||||
|
this.fetchProtocolAgendaPromise()
|
||||||
|
.then((result) => {
|
||||||
|
this.origin = result.data;
|
||||||
|
this.agenda = cloneDeep(this.origin);
|
||||||
|
this.loading = "fetched";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.loading = "failed";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fetchProtocolAgendaPromise() {
|
||||||
|
const protocolId = useProtocolStore().activeProtocol;
|
||||||
|
return http.get(`/admin/protocol/${protocolId}/agenda`);
|
||||||
|
},
|
||||||
|
createProtocolAgenda() {
|
||||||
|
const protocolId = useProtocolStore().activeProtocol;
|
||||||
|
if (protocolId == null) return;
|
||||||
|
return http
|
||||||
|
.post(`/admin/protocol/${protocolId}/agenda`)
|
||||||
|
.then((res) => {
|
||||||
|
this.agenda.push({
|
||||||
|
id: Number(res.data),
|
||||||
|
topic: "",
|
||||||
|
context: "",
|
||||||
|
protocolId: Number(protocolId),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {});
|
||||||
|
},
|
||||||
|
async synchronizeActiveProtocolAgenda() {
|
||||||
|
this.syncingProtocolAgenda = "syncing";
|
||||||
|
const protocolId = useProtocolStore().activeProtocol;
|
||||||
|
|
||||||
|
await http
|
||||||
|
.patch(`/admin/protocol/${protocolId}/synchronize/agenda`, {
|
||||||
|
agenda: differenceWith(this.agenda, this.origin, isEqual),
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
this.syncingProtocolAgenda = "synced";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.syncingProtocolAgenda = "failed";
|
||||||
|
});
|
||||||
|
this.fetchProtocolAgendaPromise()
|
||||||
|
.then((res) => {
|
||||||
|
this.origin = res.data;
|
||||||
|
if (this.detectedChangeProtocolAgenda) this.syncingProtocolAgenda = "detectedChanges";
|
||||||
|
})
|
||||||
|
.catch((err) => {});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
83
src/stores/admin/protocolDecision.ts
Normal file
83
src/stores/admin/protocolDecision.ts
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { http } from "@/serverCom";
|
||||||
|
import type { AxiosResponse } from "axios";
|
||||||
|
import type {
|
||||||
|
ProtocolDecisionViewModel,
|
||||||
|
SyncProtocolDecisionViewModel,
|
||||||
|
} from "../../viewmodels/admin/protocolDecision.models";
|
||||||
|
import { useProtocolStore } from "./protocol";
|
||||||
|
import cloneDeep from "lodash.clonedeep";
|
||||||
|
import isEqual from "lodash.isEqual";
|
||||||
|
import differenceWith from "lodash.differencewith";
|
||||||
|
|
||||||
|
export const useProtocolDecisionStore = defineStore("protocolDecision", {
|
||||||
|
state: () => {
|
||||||
|
return {
|
||||||
|
decision: [] as Array<ProtocolDecisionViewModel>,
|
||||||
|
origin: [] as Array<ProtocolDecisionViewModel>,
|
||||||
|
loading: "loading" as "loading" | "fetched" | "failed",
|
||||||
|
syncingProtocolDecision: "synced" as "synced" | "syncing" | "detectedChanges" | "failed",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
detectedChangeProtocolDecision: (state) =>
|
||||||
|
!isEqual(state.origin, state.decision) && state.syncingProtocolDecision != "syncing",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
setProtocolDecisionSyncingState(state: "synced" | "syncing" | "detectedChanges" | "failed") {
|
||||||
|
this.syncingProtocolDecision = state;
|
||||||
|
},
|
||||||
|
fetchProtocolDecision() {
|
||||||
|
this.loading = "loading";
|
||||||
|
this.fetchProtocolDecisionPromise()
|
||||||
|
.then((result) => {
|
||||||
|
this.origin = result.data;
|
||||||
|
this.decision = cloneDeep(this.origin);
|
||||||
|
this.loading = "fetched";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.loading = "failed";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fetchProtocolDecisionPromise() {
|
||||||
|
const protocolId = useProtocolStore().activeProtocol;
|
||||||
|
return http.get(`/admin/protocol/${protocolId}/decisions`);
|
||||||
|
},
|
||||||
|
createProtocolDecision() {
|
||||||
|
const protocolId = useProtocolStore().activeProtocol;
|
||||||
|
if (protocolId == null) return;
|
||||||
|
return http
|
||||||
|
.post(`/admin/protocol/${protocolId}/decision`)
|
||||||
|
.then((res) => {
|
||||||
|
this.decision.push({
|
||||||
|
id: Number(res.data),
|
||||||
|
topic: "",
|
||||||
|
context: "",
|
||||||
|
protocolId: Number(protocolId),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {});
|
||||||
|
},
|
||||||
|
async synchronizeActiveProtocolDecision() {
|
||||||
|
this.syncingProtocolDecision = "syncing";
|
||||||
|
const protocolId = useProtocolStore().activeProtocol;
|
||||||
|
|
||||||
|
await http
|
||||||
|
.patch(`/admin/protocol/${protocolId}/synchronize/decisions`, {
|
||||||
|
decisions: differenceWith(this.decision, this.origin, isEqual),
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
this.syncingProtocolDecision = "synced";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.syncingProtocolDecision = "failed";
|
||||||
|
});
|
||||||
|
this.fetchProtocolDecisionPromise()
|
||||||
|
.then((res) => {
|
||||||
|
this.origin = res.data;
|
||||||
|
if (this.detectedChangeProtocolDecision) this.syncingProtocolDecision = "detectedChanges";
|
||||||
|
})
|
||||||
|
.catch((err) => {});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
66
src/stores/admin/protocolPresence.ts
Normal file
66
src/stores/admin/protocolPresence.ts
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { http } from "@/serverCom";
|
||||||
|
import type { AxiosResponse } from "axios";
|
||||||
|
import type {
|
||||||
|
ProtocolPresenceViewModel,
|
||||||
|
SyncProtocolPresenceViewModel,
|
||||||
|
} from "../../viewmodels/admin/protocolPresence.models";
|
||||||
|
import { useProtocolStore } from "./protocol";
|
||||||
|
import cloneDeep from "lodash.clonedeep";
|
||||||
|
import isEqual from "lodash.isEqual";
|
||||||
|
|
||||||
|
export const useProtocolPresenceStore = defineStore("protocolPresence", {
|
||||||
|
state: () => {
|
||||||
|
return {
|
||||||
|
presence: [] as Array<number>,
|
||||||
|
origin: [] as Array<number>,
|
||||||
|
loading: "loading" as "loading" | "fetched" | "failed",
|
||||||
|
syncingProtocolPresence: "synced" as "synced" | "syncing" | "detectedChanges" | "failed",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
detectedChangeProtocolPresence: (state) =>
|
||||||
|
!isEqual(state.origin, state.presence) && state.syncingProtocolPresence != "syncing",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
setProtocolPresenceSyncingState(state: "synced" | "syncing" | "detectedChanges" | "failed") {
|
||||||
|
this.syncingProtocolPresence = state;
|
||||||
|
},
|
||||||
|
fetchProtocolPresence() {
|
||||||
|
this.loading = "loading";
|
||||||
|
this.fetchProtocolPresencePromise()
|
||||||
|
.then((result) => {
|
||||||
|
this.origin = result.data.map((d: ProtocolPresenceViewModel) => d.memberId);
|
||||||
|
this.presence = cloneDeep(this.origin);
|
||||||
|
this.loading = "fetched";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.loading = "failed";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fetchProtocolPresencePromise() {
|
||||||
|
const protocolId = useProtocolStore().activeProtocol;
|
||||||
|
return http.get(`/admin/protocol/${protocolId}/presence`);
|
||||||
|
},
|
||||||
|
async synchronizeActiveProtocolPresence() {
|
||||||
|
this.syncingProtocolPresence = "syncing";
|
||||||
|
const protocolId = useProtocolStore().activeProtocol;
|
||||||
|
await http
|
||||||
|
.put(`/admin/protocol/${protocolId}/synchronize/presence`, {
|
||||||
|
presence: this.presence,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
this.syncingProtocolPresence = "synced";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.syncingProtocolPresence = "failed";
|
||||||
|
});
|
||||||
|
this.fetchProtocolPresencePromise()
|
||||||
|
.then((result) => {
|
||||||
|
this.origin = result.data.map((d: ProtocolPresenceViewModel) => d.memberId);
|
||||||
|
if (this.detectedChangeProtocolPresence) this.syncingProtocolPresence = "detectedChanges";
|
||||||
|
})
|
||||||
|
.catch((err) => {});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
55
src/stores/admin/protocolPrintout.ts
Normal file
55
src/stores/admin/protocolPrintout.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { http } from "@/serverCom";
|
||||||
|
import type { ProtocolPrintoutViewModel } from "../../viewmodels/admin/protocolPrintout.models";
|
||||||
|
import { useProtocolStore } from "./protocol";
|
||||||
|
import type { AxiosResponse } from "axios";
|
||||||
|
|
||||||
|
export const useProtocolPrintoutStore = defineStore("protocolPrintout", {
|
||||||
|
state: () => {
|
||||||
|
return {
|
||||||
|
printout: [] as Array<ProtocolPrintoutViewModel>,
|
||||||
|
loading: "loading" as "loading" | "fetched" | "failed",
|
||||||
|
printing: undefined as undefined | "loading" | "success" | "failed",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
fetchProtocolPrintout() {
|
||||||
|
const protocolId = useProtocolStore().activeProtocol;
|
||||||
|
this.loading = "loading";
|
||||||
|
http
|
||||||
|
.get(`/admin/protocol/${protocolId}/printouts`)
|
||||||
|
.then((result) => {
|
||||||
|
this.printout = result.data;
|
||||||
|
this.loading = "fetched";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.loading = "failed";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fetchProtocolPrintoutById(printoutId: number): Promise<AxiosResponse<any, any>> {
|
||||||
|
const protocolId = useProtocolStore().activeProtocol;
|
||||||
|
return http.get(`/admin/protocol/${protocolId}/printout/${printoutId}`, {
|
||||||
|
responseType: "blob",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
createProtocolPrintout() {
|
||||||
|
this.printing = "loading";
|
||||||
|
const protocolId = useProtocolStore().activeProtocol;
|
||||||
|
if (protocolId == null) return;
|
||||||
|
return http
|
||||||
|
.post(`/admin/protocol/${protocolId}/printout`)
|
||||||
|
.then((res) => {
|
||||||
|
this.fetchProtocolPrintout();
|
||||||
|
this.printing = "success";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.printing = "failed";
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.printing = undefined;
|
||||||
|
}, 1500);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
86
src/stores/admin/protocolVoting.ts
Normal file
86
src/stores/admin/protocolVoting.ts
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { http } from "@/serverCom";
|
||||||
|
import type { AxiosResponse } from "axios";
|
||||||
|
import type {
|
||||||
|
ProtocolVotingViewModel,
|
||||||
|
SyncProtocolVotingViewModel,
|
||||||
|
} from "../../viewmodels/admin/protocolVoting.models";
|
||||||
|
import { useProtocolStore } from "./protocol";
|
||||||
|
import cloneDeep from "lodash.clonedeep";
|
||||||
|
import isEqual from "lodash.isEqual";
|
||||||
|
import differenceWith from "lodash.differencewith";
|
||||||
|
|
||||||
|
export const useProtocolVotingStore = defineStore("protocolVoting", {
|
||||||
|
state: () => {
|
||||||
|
return {
|
||||||
|
voting: [] as Array<ProtocolVotingViewModel>,
|
||||||
|
origin: [] as Array<ProtocolVotingViewModel>,
|
||||||
|
loading: "loading" as "loading" | "fetched" | "failed",
|
||||||
|
syncingProtocolVoting: "synced" as "synced" | "syncing" | "detectedChanges" | "failed",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
detectedChangeProtocolVoting: (state) =>
|
||||||
|
!isEqual(state.origin, state.voting) && state.syncingProtocolVoting != "syncing",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
setProtocolVotingSyncingState(state: "synced" | "syncing" | "detectedChanges" | "failed") {
|
||||||
|
this.syncingProtocolVoting = state;
|
||||||
|
},
|
||||||
|
fetchProtocolVoting() {
|
||||||
|
this.loading = "loading";
|
||||||
|
this.fetchProtocolVotingPromise()
|
||||||
|
.then((result) => {
|
||||||
|
this.origin = result.data;
|
||||||
|
this.voting = cloneDeep(this.origin);
|
||||||
|
this.loading = "fetched";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.loading = "failed";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fetchProtocolVotingPromise() {
|
||||||
|
const protocolId = useProtocolStore().activeProtocol;
|
||||||
|
return http.get(`/admin/protocol/${protocolId}/votings`);
|
||||||
|
},
|
||||||
|
createProtocolVoting() {
|
||||||
|
const protocolId = useProtocolStore().activeProtocol;
|
||||||
|
if (protocolId == null) return;
|
||||||
|
return http
|
||||||
|
.post(`/admin/protocol/${protocolId}/voting`)
|
||||||
|
.then((res) => {
|
||||||
|
this.voting.push({
|
||||||
|
id: Number(res.data),
|
||||||
|
topic: "",
|
||||||
|
context: "",
|
||||||
|
favour: 0,
|
||||||
|
abstain: 0,
|
||||||
|
against: 0,
|
||||||
|
protocolId: Number(protocolId),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {});
|
||||||
|
},
|
||||||
|
async synchronizeActiveProtocolVoting() {
|
||||||
|
this.syncingProtocolVoting = "syncing";
|
||||||
|
const protocolId = useProtocolStore().activeProtocol;
|
||||||
|
|
||||||
|
await http
|
||||||
|
.patch(`/admin/protocol/${protocolId}/synchronize/votings`, {
|
||||||
|
votings: differenceWith(this.voting, this.origin, isEqual),
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
this.syncingProtocolVoting = "synced";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.syncingProtocolVoting = "failed";
|
||||||
|
});
|
||||||
|
this.fetchProtocolVotingPromise()
|
||||||
|
.then((res) => {
|
||||||
|
this.origin = res.data;
|
||||||
|
if (this.detectedChangeProtocolVoting) this.syncingProtocolVoting = "detectedChanges";
|
||||||
|
})
|
||||||
|
.catch((err) => {});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -4,7 +4,7 @@ export type PermissionModule =
|
||||||
| "member"
|
| "member"
|
||||||
| "calendar"
|
| "calendar"
|
||||||
| "newsletter"
|
| "newsletter"
|
||||||
| "protocoll"
|
| "protocol"
|
||||||
| "qualification"
|
| "qualification"
|
||||||
| "award"
|
| "award"
|
||||||
| "executive_position"
|
| "executive_position"
|
||||||
|
@ -40,7 +40,7 @@ export const permissionModules: Array<PermissionModule> = [
|
||||||
"member",
|
"member",
|
||||||
"calendar",
|
"calendar",
|
||||||
"newsletter",
|
"newsletter",
|
||||||
"protocoll",
|
"protocol",
|
||||||
"qualification",
|
"qualification",
|
||||||
"award",
|
"award",
|
||||||
"executive_position",
|
"executive_position",
|
||||||
|
@ -52,7 +52,7 @@ export const permissionModules: Array<PermissionModule> = [
|
||||||
];
|
];
|
||||||
export const permissionTypes: Array<PermissionType> = ["read", "create", "update", "delete"];
|
export const permissionTypes: Array<PermissionType> = ["read", "create", "update", "delete"];
|
||||||
export const sectionsAndModules: SectionsAndModulesObject = {
|
export const sectionsAndModules: SectionsAndModulesObject = {
|
||||||
club: ["member", "calendar", "newsletter", "protocoll"],
|
club: ["member", "calendar", "newsletter", "protocol"],
|
||||||
settings: ["qualification", "award", "executive_position", "communication", "membership_status", "calendar_type"],
|
settings: ["qualification", "award", "executive_position", "communication", "membership_status", "calendar_type"],
|
||||||
user: ["user", "role"],
|
user: ["user", "role"],
|
||||||
};
|
};
|
||||||
|
|
22
src/viewmodels/admin/protocol.models.ts
Normal file
22
src/viewmodels/admin/protocol.models.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
export interface ProtocolViewModel {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
date: Date;
|
||||||
|
starttime: Date;
|
||||||
|
endtime: Date;
|
||||||
|
summary: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateProtocolViewModel {
|
||||||
|
title: string;
|
||||||
|
date: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncProtocolViewModel {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
date: Date;
|
||||||
|
starttime: Date;
|
||||||
|
endtime: Date;
|
||||||
|
summary: string;
|
||||||
|
}
|
12
src/viewmodels/admin/protocolAgenda.models.ts
Normal file
12
src/viewmodels/admin/protocolAgenda.models.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export interface ProtocolAgendaViewModel {
|
||||||
|
id: number;
|
||||||
|
topic: string;
|
||||||
|
context: string;
|
||||||
|
protocolId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncProtocolAgendaViewModel {
|
||||||
|
id?: number;
|
||||||
|
topic: string;
|
||||||
|
context: string;
|
||||||
|
}
|
12
src/viewmodels/admin/protocolDecision.models.ts
Normal file
12
src/viewmodels/admin/protocolDecision.models.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export interface ProtocolDecisionViewModel {
|
||||||
|
id: number;
|
||||||
|
topic: string;
|
||||||
|
context: string;
|
||||||
|
protocolId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncProtocolDecisionViewModel {
|
||||||
|
id?: number;
|
||||||
|
topic: string;
|
||||||
|
context: string;
|
||||||
|
}
|
11
src/viewmodels/admin/protocolPresence.models.ts
Normal file
11
src/viewmodels/admin/protocolPresence.models.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import type { MemberViewModel } from "./member.models";
|
||||||
|
|
||||||
|
export interface ProtocolPresenceViewModel {
|
||||||
|
memberId: number;
|
||||||
|
member: MemberViewModel;
|
||||||
|
protocolId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncProtocolPresenceViewModel {
|
||||||
|
memberIds: Array<number>;
|
||||||
|
}
|
7
src/viewmodels/admin/protocolPrintout.models.ts
Normal file
7
src/viewmodels/admin/protocolPrintout.models.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export interface ProtocolPrintoutViewModel {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
iteration: number;
|
||||||
|
createdAt: Date;
|
||||||
|
protocolId: number;
|
||||||
|
}
|
19
src/viewmodels/admin/protocolVoting.models.ts
Normal file
19
src/viewmodels/admin/protocolVoting.models.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
export interface ProtocolVotingViewModel {
|
||||||
|
id: number;
|
||||||
|
topic: string;
|
||||||
|
context: string;
|
||||||
|
favour: number;
|
||||||
|
abstain: number;
|
||||||
|
against: number;
|
||||||
|
protocolId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncProtocolVotingViewModel {
|
||||||
|
id?: number;
|
||||||
|
topic: string;
|
||||||
|
context: string;
|
||||||
|
favour: number;
|
||||||
|
abstain: number;
|
||||||
|
against: number;
|
||||||
|
protocolId: number;
|
||||||
|
}
|
|
@ -12,7 +12,15 @@
|
||||||
<input id="username" name="username" type="text" required placeholder="Benutzer" class="!rounded-b-none" />
|
<input id="username" name="username" type="text" required placeholder="Benutzer" class="!rounded-b-none" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input id="totp" name="totp" type="text" required placeholder="TOTP" class="!rounded-t-none" />
|
<input
|
||||||
|
id="totp"
|
||||||
|
name="totp"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="TOTP"
|
||||||
|
class="!rounded-t-none"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -69,8 +77,8 @@ export default defineComponent({
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
this.loginStatus = "success";
|
this.loginStatus = "success";
|
||||||
localStorage.setItem("accessToken", result.data.accessToken),
|
localStorage.setItem("accessToken", result.data.accessToken);
|
||||||
localStorage.setItem("refreshToken", result.data.refreshToken),
|
localStorage.setItem("refreshToken", result.data.refreshToken);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$router.push(`/admin`);
|
this.$router.push(`/admin`);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
66
src/views/admin/club/protocol/Protocol.vue
Normal file
66
src/views/admin/club/protocol/Protocol.vue
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<template>
|
||||||
|
<MainTemplate>
|
||||||
|
<template #topBar>
|
||||||
|
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
||||||
|
<h1 class="font-bold text-xl h-8">Protokolle</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #diffMain>
|
||||||
|
<div class="flex flex-col w-full h-full gap-2 justify-center px-7">
|
||||||
|
<Pagination
|
||||||
|
:items="protocols"
|
||||||
|
:totalCount="totalCount"
|
||||||
|
:indicateLoading="loading == 'loading'"
|
||||||
|
@load-data="(offset, count, search) => fetchProtocols(offset, count)"
|
||||||
|
@search="(search) => fetchProtocols(0, 25, true)"
|
||||||
|
>
|
||||||
|
<template #pageRow="{ row }: { row: ProtocolViewModel }">
|
||||||
|
<ProtocolListItem :protocol="row" />
|
||||||
|
</template>
|
||||||
|
</Pagination>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-4">
|
||||||
|
<button primary class="!w-fit" @click="openCreateModal">Protokoll erstellen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MainTemplate>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
|
||||||
|
import { mapActions, mapState } from "pinia";
|
||||||
|
import MainTemplate from "@/templates/Main.vue";
|
||||||
|
import { ChevronRightIcon, ChevronLeftIcon } from "@heroicons/vue/20/solid";
|
||||||
|
import { useProtocolStore } from "@/stores/admin/protocol";
|
||||||
|
import ProtocolListItem from "@/components/admin/club/protocol/ProtocolListItem.vue";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import Pagination from "../../../../components/Pagination.vue";
|
||||||
|
import type { ProtocolViewModel } from "../../../../viewmodels/admin/protocol.models";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentPage: 0,
|
||||||
|
maxEntriesPerPage: 25,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useProtocolStore, ["protocols", "totalCount", "loading"]),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchProtocols(0, this.maxEntriesPerPage, true);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useProtocolStore, ["fetchProtocols"]),
|
||||||
|
...mapActions(useModalStore, ["openModal"]),
|
||||||
|
openCreateModal() {
|
||||||
|
this.openModal(
|
||||||
|
markRaw(defineAsyncComponent(() => import("@/components/admin/club/protocol/CreateProtocolModal.vue")))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
74
src/views/admin/club/protocol/ProtocolAgenda.vue
Normal file
74
src/views/admin/club/protocol/ProtocolAgenda.vue
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||||
|
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||||
|
<p v-else-if="loading == 'failed'" @click="fetchProtocolAgenda" class="cursor-pointer">
|
||||||
|
↺ laden fehlgeschlagen
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 h-full overflow-y-auto">
|
||||||
|
<details
|
||||||
|
v-for="item in agenda"
|
||||||
|
class="flex flex-col gap-2 rounded-lg w-full justify-between border border-primary overflow-hidden min-h-fit"
|
||||||
|
>
|
||||||
|
<summary class="flex flex-row gap-2 bg-primary p-2 w-full justify-between items-center cursor-pointer">
|
||||||
|
<svg
|
||||||
|
class="fill-white stroke-white opacity-75 w-4 h-4"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path d="M12.95 10.707l.707-.707L8 4.343 6.586 5.757 10.828 10l-4.242 4.243L8 15.657l4.95-4.95z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
id="title"
|
||||||
|
placeholder="TOP"
|
||||||
|
autocomplete="off"
|
||||||
|
v-model="item.topic"
|
||||||
|
@keyup.prevent
|
||||||
|
/>
|
||||||
|
</summary>
|
||||||
|
<QuillEditor
|
||||||
|
id="top"
|
||||||
|
theme="snow"
|
||||||
|
placeholder="TOP Inhalt..."
|
||||||
|
style="height: 250px; max-height: 250px; min-height: 250px"
|
||||||
|
contentType="html"
|
||||||
|
:toolbar="toolbarOptions"
|
||||||
|
v-model:content="item.context"
|
||||||
|
/>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button primary class="!w-fit" @click="createProtocolAgenda">Eintrag hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import { QuillEditor } from "@vueup/vue-quill";
|
||||||
|
import "@vueup/vue-quill/dist/vue-quill.snow.css";
|
||||||
|
import { toolbarOptions } from "@/helpers/quillConfig";
|
||||||
|
import { useProtocolAgendaStore } from "@/stores/admin/protocolAgenda";
|
||||||
|
import type { ProtocolAgendaViewModel } from "@/viewmodels/admin/protocolAgenda.models";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
protocolId: String,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapWritableState(useProtocolAgendaStore, ["agenda", "loading"]),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchProtocolAgenda();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useProtocolAgendaStore, ["fetchProtocolAgenda", "createProtocolAgenda"]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
74
src/views/admin/club/protocol/ProtocolDecisions.vue
Normal file
74
src/views/admin/club/protocol/ProtocolDecisions.vue
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||||
|
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||||
|
<p v-else-if="loading == 'failed'" @click="fetchProtocolDecision" class="cursor-pointer">
|
||||||
|
↺ laden fehlgeschlagen
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 h-full overflow-y-auto">
|
||||||
|
<details
|
||||||
|
v-for="item in decision"
|
||||||
|
class="flex flex-col gap-2 rounded-lg w-full justify-between border border-primary overflow-hidden min-h-fit"
|
||||||
|
>
|
||||||
|
<summary class="flex flex-row gap-2 bg-primary p-2 w-full justify-between items-center cursor-pointer">
|
||||||
|
<svg
|
||||||
|
class="fill-white stroke-white opacity-75 w-4 h-4"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path d="M12.95 10.707l.707-.707L8 4.343 6.586 5.757 10.828 10l-4.242 4.243L8 15.657l4.95-4.95z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
id="title"
|
||||||
|
placeholder="Einscheidung"
|
||||||
|
autocomplete="off"
|
||||||
|
v-model="item.topic"
|
||||||
|
@keyup.prevent
|
||||||
|
/>
|
||||||
|
</summary>
|
||||||
|
<QuillEditor
|
||||||
|
id="top"
|
||||||
|
theme="snow"
|
||||||
|
placeholder="Entscheidung Inhalt..."
|
||||||
|
style="height: 250px; max-height: 250px; min-height: 250px"
|
||||||
|
contentType="html"
|
||||||
|
:toolbar="toolbarOptions"
|
||||||
|
v-model:content="item.context"
|
||||||
|
/>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button primary class="!w-fit" @click="createProtocolDecision">Eintrag hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import { useProtocolStore } from "@/stores/admin/protocol";
|
||||||
|
import { QuillEditor } from "@vueup/vue-quill";
|
||||||
|
import "@vueup/vue-quill/dist/vue-quill.snow.css";
|
||||||
|
import { toolbarOptions } from "@/helpers/quillConfig";
|
||||||
|
import { useProtocolDecisionStore } from "../../../../stores/admin/protocolDecision";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
protocolId: String,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapWritableState(useProtocolDecisionStore, ["decision", "loading"]),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchProtocolDecision();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useProtocolDecisionStore, ["fetchProtocolDecision", "createProtocolDecision"]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
68
src/views/admin/club/protocol/ProtocolOverview.vue
Normal file
68
src/views/admin/club/protocol/ProtocolOverview.vue
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||||
|
<div v-if="activeProtocolObj != null" class="flex flex-col gap-2 w-full">
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="title">Titel</label>
|
||||||
|
<input type="text" id="title" v-model="activeProtocolObj.title" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="date">Datum</label>
|
||||||
|
<input type="date" id="date" v-model="activeProtocolObj.date" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row gap-2 w-full">
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="starttime">Startzeit</label>
|
||||||
|
<input type="time" id="starttime" step="1" v-model="activeProtocolObj.starttime" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="endtime">Endzeit</label>
|
||||||
|
<input type="time" id="endtime" step="1" v-model="activeProtocolObj.endtime" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col h-1/2">
|
||||||
|
<label for="summary">Zusammenfassung</label>
|
||||||
|
<QuillEditor
|
||||||
|
id="summary"
|
||||||
|
theme="snow"
|
||||||
|
placeholder="Zusammenfassung zur Sitzung..."
|
||||||
|
style="height: 250px; max-height: 250px; min-height: 250px"
|
||||||
|
contentType="html"
|
||||||
|
:toolbar="toolbarOptions"
|
||||||
|
v-model:content="activeProtocolObj.summary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Spinner v-if="loadingActive == 'loading'" class="mx-auto" />
|
||||||
|
<p v-else-if="loadingActive == 'failed'" @click="fetchProtocolByActiveId" class="cursor-pointer">
|
||||||
|
↺ laden fehlgeschlagen
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import { useProtocolStore } from "@/stores/admin/protocol";
|
||||||
|
import { QuillEditor } from "@vueup/vue-quill";
|
||||||
|
import "@vueup/vue-quill/dist/vue-quill.snow.css";
|
||||||
|
import { toolbarOptions } from "@/helpers/quillConfig";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
protocolId: String,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapWritableState(useProtocolStore, ["loadingActive", "activeProtocolObj"]),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchProtocolByActiveId();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useProtocolStore, ["fetchProtocolByActiveId"]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
152
src/views/admin/club/protocol/ProtocolPresence.vue
Normal file
152
src/views/admin/club/protocol/ProtocolPresence.vue
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||||
|
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||||
|
<p v-else-if="loading == 'failed'" @click="fetchProtocolPresence" class="cursor-pointer">
|
||||||
|
↺ laden fehlgeschlagen
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<Combobox v-model="presence" multiple>
|
||||||
|
<ComboboxLabel>Anwesende suchen</ComboboxLabel>
|
||||||
|
<div class="relative mt-1">
|
||||||
|
<ComboboxInput
|
||||||
|
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||||
|
@input="query = $event.target.value"
|
||||||
|
/>
|
||||||
|
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
|
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||||
|
</ComboboxButton>
|
||||||
|
<TransitionRoot
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
@after-leave="query = ''"
|
||||||
|
>
|
||||||
|
<ComboboxOptions
|
||||||
|
class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-md ring-1 ring-black/5 focus:outline-none sm:text-sm"
|
||||||
|
>
|
||||||
|
<ComboboxOption v-if="filtered.length === 0" as="template" disabled>
|
||||||
|
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
|
||||||
|
<span class="font-normal block truncate"> Keine Auswahl</span>
|
||||||
|
</li>
|
||||||
|
</ComboboxOption>
|
||||||
|
|
||||||
|
<ComboboxOption
|
||||||
|
v-for="member in filtered"
|
||||||
|
as="template"
|
||||||
|
:key="member.id"
|
||||||
|
:value="member.id"
|
||||||
|
v-slot="{ selected, active }"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="relative cursor-default select-none py-2 pl-10 pr-4"
|
||||||
|
:class="{
|
||||||
|
'bg-primary text-white': active,
|
||||||
|
'text-gray-900': !active,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="block truncate" :class="{ 'font-medium': selected, 'font-normal': !selected }">
|
||||||
|
{{ member.firstname }} {{ member.lastname }} {{ member.nameaffix }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="selected"
|
||||||
|
class="absolute inset-y-0 left-0 flex items-center pl-3"
|
||||||
|
:class="{ 'text-white': active, 'text-primary': !active }"
|
||||||
|
>
|
||||||
|
<CheckIcon class="h-5 w-5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ComboboxOption>
|
||||||
|
</ComboboxOptions>
|
||||||
|
</TransitionRoot>
|
||||||
|
</div>
|
||||||
|
</Combobox>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<p>Ausgewählte Anwesende</p>
|
||||||
|
<div class="flex flex-col gap-2 grow overflow-y-auto">
|
||||||
|
<div
|
||||||
|
v-for="member in selected"
|
||||||
|
:key="member.id"
|
||||||
|
class="flex flex-row h-fit w-full border border-primary rounded-md bg-primary p-2 text-white justify-between items-center"
|
||||||
|
>
|
||||||
|
<p>{{ member.lastname }}, {{ member.firstname }} {{ member.nameaffix ? `- ${member.nameaffix}` : "" }}</p>
|
||||||
|
<TrashIcon class="w-5 h-5 p-1 box-content cursor-pointer" @click="removeSelected(member.id)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import {
|
||||||
|
Combobox,
|
||||||
|
ComboboxLabel,
|
||||||
|
ComboboxInput,
|
||||||
|
ComboboxButton,
|
||||||
|
ComboboxOptions,
|
||||||
|
ComboboxOption,
|
||||||
|
TransitionRoot,
|
||||||
|
} from "@headlessui/vue";
|
||||||
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
|
import { TrashIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import { useProtocolStore } from "@/stores/admin/protocol";
|
||||||
|
import { useMemberStore } from "@/stores/admin/member";
|
||||||
|
import type { MemberViewModel } from "@/viewmodels/admin/member.models";
|
||||||
|
import { useProtocolPresenceStore } from "../../../../stores/admin/protocolPresence";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
protocolId: String,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
query: "" as String,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapWritableState(useProtocolPresenceStore, ["presence", "loading"]),
|
||||||
|
...mapState(useMemberStore, ["members"]),
|
||||||
|
filtered(): Array<MemberViewModel> {
|
||||||
|
return this.query === ""
|
||||||
|
? this.members
|
||||||
|
: this.members.filter((member) =>
|
||||||
|
(member.firstname + " " + member.lastname)
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s+/g, "")
|
||||||
|
.includes(this.query.toLowerCase().replace(/\s+/g, ""))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
sorted(): Array<MemberViewModel> {
|
||||||
|
return this.selected.sort((a, b) => {
|
||||||
|
if (a.lastname < b.lastname) return -1;
|
||||||
|
if (a.lastname > b.lastname) return 1;
|
||||||
|
if (a.firstname < b.firstname) return -1;
|
||||||
|
if (a.firstname > b.firstname) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
selected(): Array<MemberViewModel> {
|
||||||
|
return this.members.filter((m) => this.presence.includes(m.id));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchMembers();
|
||||||
|
this.fetchProtocolPresence();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useMemberStore, ["fetchMembers"]),
|
||||||
|
...mapActions(useProtocolPresenceStore, ["fetchProtocolPresence"]),
|
||||||
|
removeSelected(id: number) {
|
||||||
|
let index = this.presence.findIndex((s) => s == id);
|
||||||
|
if (index != -1) {
|
||||||
|
this.presence.splice(index, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
94
src/views/admin/club/protocol/ProtocolPrintout.vue
Normal file
94
src/views/admin/club/protocol/ProtocolPrintout.vue
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||||
|
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||||
|
<p v-else-if="loading == 'failed'" @click="fetchProtocolPrintout" class="cursor-pointer">
|
||||||
|
↺ laden fehlgeschlagen
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 h-full overflow-y-auto">
|
||||||
|
<div
|
||||||
|
v-for="print in printout"
|
||||||
|
:key="print.id"
|
||||||
|
class="flex flex-col h-fit w-full border border-primary rounded-md"
|
||||||
|
>
|
||||||
|
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
|
||||||
|
<p>{{ print.title }}</p>
|
||||||
|
<div class="flex flex-row">
|
||||||
|
<div>
|
||||||
|
<ViewfinderCircleIcon class="w-5 h-5 p-1 box-content cursor-pointer" @click="openPdfShow(print.id)" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ArrowDownTrayIcon class="w-5 h-5 p-1 box-content cursor-pointer" @click="downloadPdf(print.id)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-2">
|
||||||
|
<p>Ausdruck Nummer: {{ print.iteration }}</p>
|
||||||
|
<p>Ausdruck erstellt: {{ print.createdAt }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-start gap-2">
|
||||||
|
<button primary class="!w-fit" :disabled="printing != undefined" @click="createProtocolPrintout">
|
||||||
|
Ausdruck erstellen
|
||||||
|
</button>
|
||||||
|
<Spinner v-if="printing == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="printing == 'success'" />
|
||||||
|
<FailureXMark v-else-if="printing == 'failed'" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
|
||||||
|
import { mapActions, mapState } from "pinia";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import { useProtocolPrintoutStore } from "@/stores/admin/protocolPrintout";
|
||||||
|
import { ArrowDownTrayIcon, ViewfinderCircleIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
protocolId: String,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useProtocolPrintoutStore, ["printout", "loading", "printing"]),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchProtocolPrintout();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useModalStore, ["openModal"]),
|
||||||
|
...mapActions(useProtocolPrintoutStore, [
|
||||||
|
"fetchProtocolPrintout",
|
||||||
|
"createProtocolPrintout",
|
||||||
|
"fetchProtocolPrintoutById",
|
||||||
|
]),
|
||||||
|
openPdfShow(id: number) {
|
||||||
|
this.openModal(
|
||||||
|
markRaw(defineAsyncComponent(() => import("@/components/admin/club/protocol/ProtocolPrintoutViewerModal.vue"))),
|
||||||
|
id
|
||||||
|
);
|
||||||
|
},
|
||||||
|
downloadPdf(id: number) {
|
||||||
|
let clickedOn = this.printout.find((p) => p.id == id);
|
||||||
|
this.fetchProtocolPrintoutById(id)
|
||||||
|
.then((response) => {
|
||||||
|
const fileURL = window.URL.createObjectURL(new Blob([response.data]));
|
||||||
|
const fileLink = document.createElement("a");
|
||||||
|
fileLink.href = fileURL;
|
||||||
|
fileLink.setAttribute("download", clickedOn?.title ? clickedOn.title + ".pdf" : "Protokoll.pdf");
|
||||||
|
document.body.appendChild(fileLink);
|
||||||
|
fileLink.click();
|
||||||
|
fileLink.remove();
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
120
src/views/admin/club/protocol/ProtocolRouting.vue
Normal file
120
src/views/admin/club/protocol/ProtocolRouting.vue
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
<template>
|
||||||
|
<MainTemplate>
|
||||||
|
<template #headerInsert>
|
||||||
|
<RouterLink to="../" class="text-primary w-fit">zurück zur Liste</RouterLink>
|
||||||
|
</template>
|
||||||
|
<template #topBar>
|
||||||
|
<div class="flex flex-row gap-2 items-center justify-between pt-5 pb-3 px-7">
|
||||||
|
<h1 class="font-bold text-xl h-8 min-h-fit grow">{{ origin?.title }}, {{ origin?.date }}</h1>
|
||||||
|
<ProtocolSyncing
|
||||||
|
:executeSyncAll="executeSyncAll"
|
||||||
|
@syncState="
|
||||||
|
(state) => {
|
||||||
|
syncState = state;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<!-- <TrashIcon class="w-5 h-5 cursor-pointer" @click="openDeleteModal" /> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #diffMain>
|
||||||
|
<div class="flex flex-col gap-2 grow px-7 overflow-hidden">
|
||||||
|
<div class="flex flex-col grow gap-2 overflow-hidden">
|
||||||
|
<div class="w-full flex flex-row max-lg:flex-wrap justify-center">
|
||||||
|
<RouterLink
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.route"
|
||||||
|
v-slot="{ isActive }"
|
||||||
|
:to="{ name: tab.route }"
|
||||||
|
class="w-1/2 md:w-1/3 lg:w-full p-0.5 first:pl-0 last:pr-0"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
:class="[
|
||||||
|
'w-full rounded-lg py-2.5 text-sm text-center font-medium leading-5 focus:ring-0 outline-none',
|
||||||
|
isActive ? 'bg-red-200 shadow border-b-2 border-primary rounded-b-none' : ' hover:bg-red-200',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ tab.title }}
|
||||||
|
</p>
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
<RouterView />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MainTemplate>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
|
||||||
|
import { mapActions, mapState } from "pinia";
|
||||||
|
import MainTemplate from "@/templates/Main.vue";
|
||||||
|
import { RouterLink, RouterView } from "vue-router";
|
||||||
|
import { useProtocolStore } from "@/stores/admin/protocol";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import ProtocolSyncing from "@/components/admin/club/protocol/ProtocolSyncing.vue";
|
||||||
|
import { PrinterIcon } from "@heroicons/vue/24/outline";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
protocolId: String,
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
syncState() {
|
||||||
|
if (this.wantToClose && this.syncState == "synced") {
|
||||||
|
this.wantToClose = false;
|
||||||
|
this.closeModal();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tabs: [
|
||||||
|
{ route: "admin-club-protocol-overview", title: "Übersicht" },
|
||||||
|
{ route: "admin-club-protocol-presence", title: "Anwesenheit" },
|
||||||
|
{ route: "admin-club-protocol-voting", title: "Abstimmungen" },
|
||||||
|
{ route: "admin-club-protocol-decisions", title: "Beschlüsse" },
|
||||||
|
{ route: "admin-club-protocol-agenda", title: "Protokoll" },
|
||||||
|
{ route: "admin-club-protocol-printout", title: "Druck" },
|
||||||
|
],
|
||||||
|
wantToClose: false as boolean,
|
||||||
|
executeSyncAll: undefined as any,
|
||||||
|
syncState: "synced" as "synced" | "syncing" | "detectedChanges" | "failed",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useProtocolStore, ["origin"]),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchProtocolByActiveId();
|
||||||
|
},
|
||||||
|
// this.syncState is undefined, so it will never work
|
||||||
|
// beforeRouteLeave(to, from, next) {
|
||||||
|
// if (this.syncState != "synced") {
|
||||||
|
// this.executeSyncAll = Date.now();
|
||||||
|
// this.wantToClose = true;
|
||||||
|
// this.openInfoModal();
|
||||||
|
// next(false);
|
||||||
|
// } else {
|
||||||
|
// next();
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
methods: {
|
||||||
|
...mapActions(useProtocolStore, ["fetchProtocolByActiveId"]),
|
||||||
|
...mapActions(useModalStore, ["openModal"]),
|
||||||
|
openInfoModal() {
|
||||||
|
this.openModal(
|
||||||
|
markRaw(defineAsyncComponent(() => import("@/components/admin/club/protocol/CurrentlySyncingModal.vue")))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
openDeleteModal() {
|
||||||
|
// this.openModal(
|
||||||
|
// markRaw(defineAsyncComponent(() => import("@/components/admin/club/protocol/DeleteProtocolModal.vue"))),
|
||||||
|
// parseInt(this.protocolId ?? "")
|
||||||
|
// );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
91
src/views/admin/club/protocol/ProtocolVoting.vue
Normal file
91
src/views/admin/club/protocol/ProtocolVoting.vue
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||||
|
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||||
|
<p v-else-if="loading == 'failed'" @click="fetchProtocolVoting" class="cursor-pointer">
|
||||||
|
↺ laden fehlgeschlagen
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 h-full overflow-y-auto">
|
||||||
|
<details
|
||||||
|
v-for="item in voting"
|
||||||
|
class="flex flex-col gap-2 rounded-lg w-full justify-between border border-primary overflow-hidden min-h-fit"
|
||||||
|
>
|
||||||
|
<summary class="flex flex-row gap-2 bg-primary p-2 w-full justify-between items-center cursor-pointer">
|
||||||
|
<svg
|
||||||
|
class="fill-white stroke-white opacity-75 w-4 h-4"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path d="M12.95 10.707l.707-.707L8 4.343 6.586 5.757 10.828 10l-4.242 4.243L8 15.657l4.95-4.95z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
id="title"
|
||||||
|
placeholder="Einscheidung"
|
||||||
|
autocomplete="off"
|
||||||
|
v-model="item.topic"
|
||||||
|
@keyup.prevent
|
||||||
|
/>
|
||||||
|
</summary>
|
||||||
|
<QuillEditor
|
||||||
|
id="top"
|
||||||
|
theme="snow"
|
||||||
|
placeholder="Entscheidung Inhalt..."
|
||||||
|
style="height: 100px; max-height: 100px; min-height: 100px"
|
||||||
|
contentType="html"
|
||||||
|
:toolbar="toolbarOptions"
|
||||||
|
v-model:content="item.context"
|
||||||
|
/>
|
||||||
|
<div class="px-2 pb-2">
|
||||||
|
<p>Ergebnis:</p>
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<div class="w-full">
|
||||||
|
<p>dafür</p>
|
||||||
|
<input type="number" v-model="item.favour" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<p>dagegen</p>
|
||||||
|
<input type="number" v-model="item.against" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<p>enthalten</p>
|
||||||
|
<input type="number" v-model="item.abstain" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button primary class="!w-fit" @click="createProtocolVoting">Abstimmung hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapActions, mapState } from "pinia";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import { useProtocolStore } from "@/stores/admin/protocol";
|
||||||
|
import { QuillEditor } from "@vueup/vue-quill";
|
||||||
|
import "@vueup/vue-quill/dist/vue-quill.snow.css";
|
||||||
|
import { toolbarOptions } from "@/helpers/quillConfig";
|
||||||
|
import { useProtocolVotingStore } from "../../../../stores/admin/protocolVoting";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
protocolId: String,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useProtocolVotingStore, ["voting", "loading"]),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchProtocolVoting();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useProtocolVotingStore, ["fetchProtocolVoting", "createProtocolVoting"]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -7,42 +7,17 @@
|
||||||
</template>
|
</template>
|
||||||
<template #diffMain>
|
<template #diffMain>
|
||||||
<div class="flex flex-col w-full h-full gap-2 justify-center px-7">
|
<div class="flex flex-col w-full h-full gap-2 justify-center px-7">
|
||||||
<div class="flex flex-col w-full grow gap-2 pr-2 overflow-y-scroll">
|
<Pagination
|
||||||
<MemberListItem v-for="member in members" :key="member.id" :member="member" />
|
:items="members"
|
||||||
</div>
|
:totalCount="totalCount"
|
||||||
<div class="flex flex-row w-full justify-between select-none">
|
:indicateLoading="loading == 'loading'"
|
||||||
<p class="text-sm font-normal text-gray-500">
|
@load-data="(offset, count, search) => fetchMembers(offset, count)"
|
||||||
Elemente <span class="font-semibold text-gray-900">{{ showingText }}</span> von
|
@search="(search) => fetchMembers(0, 25, true)"
|
||||||
<span class="font-semibold text-gray-900">{{ entryCount }}</span>
|
|
||||||
</p>
|
|
||||||
<ul class="flex flex-row text-sm h-8">
|
|
||||||
<li
|
|
||||||
class="flex h-8 w-8 items-center justify-center text-gray-500 bg-white border border-gray-300 first:rounded-s-lg last:rounded-e-lg"
|
|
||||||
:class="[currentPage > 0 ? 'cursor-pointer hover:bg-gray-100 hover:text-gray-700' : 'opacity-50']"
|
|
||||||
@click="loadPage(currentPage - 1)"
|
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon class="h-4" />
|
<template #pageRow="{ row }: { row: MemberViewModel }">
|
||||||
</li>
|
<MemberListItem :member="row" />
|
||||||
<li
|
</template>
|
||||||
v-for="page in displayedPagesNumbers"
|
</Pagination>
|
||||||
:key="page"
|
|
||||||
class="flex h-8 w-8 items-center justify-center text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 first:rounded-s-lg last:rounded-e-lg"
|
|
||||||
:class="[currentPage == page ? 'font-bold border-primary' : '', page != '.' ? ' cursor-pointer' : '']"
|
|
||||||
@click="loadPage(page)"
|
|
||||||
>
|
|
||||||
{{ typeof page == "number" ? page + 1 : "..." }}
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
class="flex h-8 w-8 items-center justify-center text-gray-500 bg-white border border-gray-300 first:rounded-s-lg last:rounded-e-lg"
|
|
||||||
:class="[
|
|
||||||
currentPage + 1 < countOfPages ? 'cursor-pointer hover:bg-gray-100 hover:text-gray-700' : 'opacity-50',
|
|
||||||
]"
|
|
||||||
@click="loadPage(currentPage + 1)"
|
|
||||||
>
|
|
||||||
<ChevronRightIcon class="h-4" />
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-row gap-4">
|
<div class="flex flex-row gap-4">
|
||||||
<button primary class="!w-fit" @click="openCreateModal">Mitglied erstellen</button>
|
<button primary class="!w-fit" @click="openCreateModal">Mitglied erstellen</button>
|
||||||
|
@ -60,6 +35,8 @@ import { ChevronRightIcon, ChevronLeftIcon } from "@heroicons/vue/20/solid";
|
||||||
import { useMemberStore } from "@/stores/admin/member";
|
import { useMemberStore } from "@/stores/admin/member";
|
||||||
import MemberListItem from "@/components/admin/club/member/MemberListItem.vue";
|
import MemberListItem from "@/components/admin/club/member/MemberListItem.vue";
|
||||||
import { useModalStore } from "@/stores/modal";
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import Pagination from "../../../components/Pagination.vue";
|
||||||
|
import type { MemberViewModel } from "../../../viewmodels/admin/member.models";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -71,7 +48,7 @@ export default defineComponent({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useMemberStore, ["members", "totalCount"]),
|
...mapState(useMemberStore, ["members", "totalCount", "loading"]),
|
||||||
entryCount() {
|
entryCount() {
|
||||||
return this.totalCount ?? this.members.length;
|
return this.totalCount ?? this.members.length;
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue