]> git.localhorst.tv Git - alttp.git/commitdiff
tournament export options
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Sun, 23 Nov 2025 12:59:59 +0000 (13:59 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Sun, 23 Nov 2025 12:59:59 +0000 (13:59 +0100)
package-lock.json
package.json
resources/js/components/common/Icon.jsx
resources/js/components/tournament/Detail.jsx
resources/js/components/tournament/ExportButton.jsx [new file with mode: 0644]
resources/js/helpers/Round.js
resources/js/helpers/Tournament.js
resources/js/helpers/downloadBlob.js [new file with mode: 0644]
resources/js/helpers/permissions.js
resources/js/i18n/de.js
resources/js/i18n/en.js

index c3b0f9c7a0645c5fff39e2d75a71360e7f27c877..5ee35f467224b5b2f61a8f492dc9d7f6c59b3dc8 100644 (file)
@@ -54,6 +54,7 @@
                 "eslint": "^9.29.0",
                 "eslint-plugin-import": "^2.25.4",
                 "eslint-plugin-react": "^7.29.3",
+                "exceljs": "^4.4.0",
                 "globals": "^16.2.0",
                 "i18next": "^25.2.1",
                 "jsdom": "^26.1.0",
@@ -83,7 +84,6 @@
             "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
             "dev": true,
             "license": "MIT",
-            "peer": true,
             "engines": {
                 "node": ">=10"
             },
             "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
             "dev": true,
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "@ampproject/remapping": "^2.2.0",
                 "@babel/code-frame": "^7.27.1",
             "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.37.2.tgz",
             "integrity": "sha512-XD3LdgQpxQs5jhOOZ2HRVT+Rj59O4Suc7g2ULvZ+Yi8eCkickrkZ5JFuoDhs2ST1mNI5zSsNYgR3NGa4OUrbnw==",
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "@codemirror/state": "^6.5.0",
                 "crelt": "^1.0.6",
                 }
             ],
             "license": "MIT",
+            "peer": true,
             "engines": {
                 "node": ">=18"
             },
                 }
             ],
             "license": "MIT",
+            "peer": true,
             "engines": {
                 "node": ">=18"
             }
                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
             }
         },
+        "node_modules/@fast-csv/format": {
+            "version": "4.3.5",
+            "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz",
+            "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@types/node": "^14.0.1",
+                "lodash.escaperegexp": "^4.1.2",
+                "lodash.isboolean": "^3.0.3",
+                "lodash.isequal": "^4.5.0",
+                "lodash.isfunction": "^3.0.9",
+                "lodash.isnil": "^4.0.0"
+            }
+        },
+        "node_modules/@fast-csv/format/node_modules/@types/node": {
+            "version": "14.18.63",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz",
+            "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/@fast-csv/parse": {
+            "version": "4.3.6",
+            "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz",
+            "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@types/node": "^14.0.1",
+                "lodash.escaperegexp": "^4.1.2",
+                "lodash.groupby": "^4.6.0",
+                "lodash.isfunction": "^3.0.9",
+                "lodash.isnil": "^4.0.0",
+                "lodash.isundefined": "^3.0.1",
+                "lodash.uniq": "^4.5.0"
+            }
+        },
+        "node_modules/@fast-csv/parse/node_modules/@types/node": {
+            "version": "14.18.63",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz",
+            "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==",
+            "dev": true,
+            "license": "MIT"
+        },
         "node_modules/@fortawesome/fontawesome-common-types": {
             "version": "6.7.2",
             "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
             "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
             "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "@fortawesome/fontawesome-common-types": "6.7.2"
             },
             "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
             "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
             "license": "MIT",
+            "peer": true,
             "funding": {
                 "type": "opencollective",
                 "url": "https://opencollective.com/popperjs"
             "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
             "dev": true,
             "license": "MIT",
-            "peer": true,
             "dependencies": {
                 "@babel/code-frame": "^7.10.4",
                 "@babel/runtime": "^7.12.5",
             "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
             "dev": true,
             "license": "Apache-2.0",
-            "peer": true,
             "dependencies": {
                 "dequal": "^2.0.3"
             }
             "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
             "dev": true,
             "license": "MIT",
-            "peer": true,
             "dependencies": {
                 "ansi-styles": "^4.1.0",
                 "supports-color": "^7.1.0"
             "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
             "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
             "dev": true,
-            "license": "MIT",
-            "peer": true
+            "license": "MIT"
         },
         "node_modules/@testing-library/jest-dom": {
             "version": "6.6.3",
             "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
             "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
             "dev": true,
-            "license": "MIT",
-            "peer": true
+            "license": "MIT"
         },
         "node_modules/@types/babel__core": {
             "version": "7.20.5",
             "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
             "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "@types/prop-types": "*",
                 "csstype": "^3.0.2"
             "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
             "dev": true,
             "license": "MIT",
+            "peer": true,
             "bin": {
                 "acorn": "bin/acorn"
             },
             "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
             "dev": true,
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "fast-deep-equal": "^3.1.1",
                 "fast-json-stable-stringify": "^2.0.0",
             "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
             "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
             "dev": true,
-            "license": "MIT",
-            "peer": true
+            "license": "MIT"
         },
         "node_modules/anymatch": {
             "version": "3.1.3",
             "integrity": "sha512-ENBjy3HAyu7slaOgWRvVtFUdSGFWIHZvz8bGKSEM6/OWQm6NgwFquqY8R7YPgm4vnDISEczhZE2NoLAtKlnUfw==",
             "license": "MIT"
         },
+        "node_modules/archiver": {
+            "version": "5.3.2",
+            "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
+            "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "archiver-utils": "^2.1.0",
+                "async": "^3.2.4",
+                "buffer-crc32": "^0.2.1",
+                "readable-stream": "^3.6.0",
+                "readdir-glob": "^1.1.2",
+                "tar-stream": "^2.2.0",
+                "zip-stream": "^4.1.0"
+            },
+            "engines": {
+                "node": ">= 10"
+            }
+        },
+        "node_modules/archiver-utils": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
+            "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "glob": "^7.1.4",
+                "graceful-fs": "^4.2.0",
+                "lazystream": "^1.0.0",
+                "lodash.defaults": "^4.2.0",
+                "lodash.difference": "^4.5.0",
+                "lodash.flatten": "^4.4.0",
+                "lodash.isplainobject": "^4.0.6",
+                "lodash.union": "^4.6.0",
+                "normalize-path": "^3.0.0",
+                "readable-stream": "^2.0.0"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/archiver/node_modules/readable-stream": {
+            "version": "3.6.2",
+            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+            "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "inherits": "^2.0.3",
+                "string_decoder": "^1.1.1",
+                "util-deprecate": "^1.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
         "node_modules/arg": {
             "version": "5.0.2",
             "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
             "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
             "dev": true,
-            "license": "MIT",
-            "peer": true
+            "license": "MIT"
         },
         "node_modules/argparse": {
             "version": "2.0.1",
             "dev": true,
             "license": "MIT"
         },
+        "node_modules/async": {
+            "version": "3.2.6",
+            "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+            "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+            "dev": true,
+            "license": "MIT"
+        },
         "node_modules/async-function": {
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
             "dev": true,
             "license": "MIT"
         },
+        "node_modules/big-integer": {
+            "version": "1.6.52",
+            "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
+            "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
+            "dev": true,
+            "license": "Unlicense",
+            "engines": {
+                "node": ">=0.6"
+            }
+        },
         "node_modules/big.js": {
             "version": "5.2.2",
             "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
                 "node": "*"
             }
         },
+        "node_modules/binary": {
+            "version": "0.3.0",
+            "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
+            "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "buffers": "~0.1.1",
+                "chainsaw": "~0.1.0"
+            },
+            "engines": {
+                "node": "*"
+            }
+        },
         "node_modules/binary-extensions": {
             "version": "2.3.0",
             "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
+        "node_modules/bl": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+            "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "buffer": "^5.5.0",
+                "inherits": "^2.0.4",
+                "readable-stream": "^3.4.0"
+            }
+        },
+        "node_modules/bl/node_modules/buffer": {
+            "version": "5.7.1",
+            "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+            "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ],
+            "license": "MIT",
+            "dependencies": {
+                "base64-js": "^1.3.1",
+                "ieee754": "^1.1.13"
+            }
+        },
+        "node_modules/bl/node_modules/readable-stream": {
+            "version": "3.6.2",
+            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+            "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "inherits": "^2.0.3",
+                "string_decoder": "^1.1.1",
+                "util-deprecate": "^1.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/bluebird": {
+            "version": "3.4.7",
+            "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
+            "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==",
+            "dev": true,
+            "license": "MIT"
+        },
         "node_modules/bn.js": {
             "version": "5.2.2",
             "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz",
                 }
             ],
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "caniuse-lite": "^1.0.30001718",
                 "electron-to-chromium": "^1.5.160",
                 "isarray": "^1.0.0"
             }
         },
+        "node_modules/buffer-crc32": {
+            "version": "0.2.13",
+            "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+            "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": "*"
+            }
+        },
         "node_modules/buffer-from": {
             "version": "1.1.2",
             "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
             "dev": true,
             "license": "MIT"
         },
+        "node_modules/buffer-indexof-polyfill": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
+            "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=0.10"
+            }
+        },
         "node_modules/buffer-xor": {
             "version": "1.0.3",
             "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
             "dev": true,
             "license": "MIT"
         },
+        "node_modules/buffers": {
+            "version": "0.1.1",
+            "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
+            "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.2.0"
+            }
+        },
         "node_modules/builtin-status-codes": {
             "version": "3.0.0",
             "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
             "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
             "dev": true,
             "license": "MIT",
-            "peer": true,
             "engines": {
                 "node": ">= 6"
             }
                 "node": ">=12"
             }
         },
+        "node_modules/chainsaw": {
+            "version": "0.1.0",
+            "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
+            "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==",
+            "dev": true,
+            "license": "MIT/X11",
+            "dependencies": {
+                "traverse": ">=0.3.0 <0.4"
+            },
+            "engines": {
+                "node": "*"
+            }
+        },
         "node_modules/chalk": {
             "version": "3.0.0",
             "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
             "dev": true,
             "license": "MIT"
         },
+        "node_modules/compress-commons": {
+            "version": "4.1.2",
+            "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz",
+            "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "buffer-crc32": "^0.2.13",
+                "crc32-stream": "^4.0.2",
+                "normalize-path": "^3.0.0",
+                "readable-stream": "^3.6.0"
+            },
+            "engines": {
+                "node": ">= 10"
+            }
+        },
+        "node_modules/compress-commons/node_modules/readable-stream": {
+            "version": "3.6.2",
+            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+            "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "inherits": "^2.0.3",
+                "string_decoder": "^1.1.1",
+                "util-deprecate": "^1.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
         "node_modules/compressible": {
             "version": "2.0.18",
             "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
                 "node": ">=0.8"
             }
         },
+        "node_modules/crc32-stream": {
+            "version": "4.0.3",
+            "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz",
+            "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "crc-32": "^1.2.0",
+                "readable-stream": "^3.4.0"
+            },
+            "engines": {
+                "node": ">= 10"
+            }
+        },
+        "node_modules/crc32-stream/node_modules/readable-stream": {
+            "version": "3.6.2",
+            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+            "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "inherits": "^2.0.3",
+                "string_decoder": "^1.1.1",
+                "util-deprecate": "^1.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
         "node_modules/create-ecdh": {
             "version": "4.0.4",
             "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
+        "node_modules/dayjs": {
+            "version": "1.11.19",
+            "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
+            "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
+            "dev": true,
+            "license": "MIT"
+        },
         "node_modules/debug": {
             "version": "4.4.1",
             "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
             "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
             "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
             "dev": true,
-            "license": "Apache-2.0",
-            "peer": true
+            "license": "Apache-2.0"
         },
         "node_modules/diffie-hellman": {
             "version": "5.0.3",
             "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
             "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
             "dev": true,
-            "license": "MIT",
-            "peer": true
+            "license": "MIT"
         },
         "node_modules/dns-packet": {
             "version": "5.6.1",
                 "node": ">= 0.4"
             }
         },
+        "node_modules/duplexer2": {
+            "version": "0.1.4",
+            "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
+            "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==",
+            "dev": true,
+            "license": "BSD-3-Clause",
+            "dependencies": {
+                "readable-stream": "^2.0.2"
+            }
+        },
         "node_modules/eastasianwidth": {
             "version": "0.2.0",
             "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
                 "node": ">= 0.8"
             }
         },
+        "node_modules/end-of-stream": {
+            "version": "1.4.5",
+            "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+            "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "once": "^1.4.0"
+            }
+        },
         "node_modules/enhanced-resolve": {
             "version": "5.18.1",
             "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
             "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
             "dev": true,
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "@eslint-community/eslint-utils": "^4.2.0",
                 "@eslint-community/regexpp": "^4.12.1",
                 "safe-buffer": "^5.1.1"
             }
         },
+        "node_modules/exceljs": {
+            "version": "4.4.0",
+            "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz",
+            "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "archiver": "^5.0.0",
+                "dayjs": "^1.8.34",
+                "fast-csv": "^4.3.1",
+                "jszip": "^3.10.1",
+                "readable-stream": "^3.6.0",
+                "saxes": "^5.0.1",
+                "tmp": "^0.2.0",
+                "unzipper": "^0.10.11",
+                "uuid": "^8.3.0"
+            },
+            "engines": {
+                "node": ">=8.3.0"
+            }
+        },
+        "node_modules/exceljs/node_modules/readable-stream": {
+            "version": "3.6.2",
+            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+            "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "inherits": "^2.0.3",
+                "string_decoder": "^1.1.1",
+                "util-deprecate": "^1.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/exceljs/node_modules/saxes": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
+            "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==",
+            "dev": true,
+            "license": "ISC",
+            "dependencies": {
+                "xmlchars": "^2.2.0"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
         "node_modules/execa": {
             "version": "5.1.1",
             "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
+        "node_modules/fast-csv": {
+            "version": "4.3.6",
+            "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz",
+            "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "@fast-csv/format": "4.3.5",
+                "@fast-csv/parse": "4.3.6"
+            },
+            "engines": {
+                "node": ">=10.0.0"
+            }
+        },
         "node_modules/fast-deep-equal": {
             "version": "3.1.3",
             "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
                 "node": ">= 0.6"
             }
         },
+        "node_modules/fs-constants": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+            "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+            "dev": true,
+            "license": "MIT"
+        },
         "node_modules/fs-extra": {
             "version": "10.1.0",
             "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
                 "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
             }
         },
+        "node_modules/fstream": {
+            "version": "1.0.12",
+            "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
+            "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
+            "deprecated": "This package is no longer supported.",
+            "dev": true,
+            "license": "ISC",
+            "dependencies": {
+                "graceful-fs": "^4.1.2",
+                "inherits": "~2.0.0",
+                "mkdirp": ">=0.5 0",
+                "rimraf": "2"
+            },
+            "engines": {
+                "node": ">=0.6"
+            }
+        },
+        "node_modules/fstream/node_modules/rimraf": {
+            "version": "2.7.1",
+            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+            "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+            "deprecated": "Rimraf versions prior to v4 are no longer supported",
+            "dev": true,
+            "license": "ISC",
+            "dependencies": {
+                "glob": "^7.1.3"
+            },
+            "bin": {
+                "rimraf": "bin.js"
+            }
+        },
         "node_modules/function-bind": {
             "version": "1.1.2",
             "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
                 }
             ],
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "@babel/runtime": "^7.27.1"
             },
             "integrity": "sha512-33AmZ+xjZhg2JMCe+vDf6a9mzWukE7l+wAtesjE7KyteqqKjzxv7aVQeWnul1Ve26mWvEQqyPwl0OctNBfSR9w==",
             "dev": true,
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "file-type": "^12.0.0",
                 "globby": "^10.0.0",
             "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
             "dev": true,
             "license": "MIT",
-            "peer": true,
             "bin": {
                 "jiti": "bin/jiti.js"
             }
             "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
             "dev": true,
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "cssstyle": "^4.2.1",
                 "data-urls": "^5.0.0",
                 "node": ">=4.0"
             }
         },
+        "node_modules/jszip": {
+            "version": "3.10.1",
+            "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+            "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+            "dev": true,
+            "license": "(MIT OR GPL-3.0-or-later)",
+            "dependencies": {
+                "lie": "~3.3.0",
+                "pako": "~1.0.2",
+                "readable-stream": "~2.3.6",
+                "setimmediate": "^1.0.5"
+            }
+        },
+        "node_modules/jszip/node_modules/lie": {
+            "version": "3.3.0",
+            "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+            "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "immediate": "~3.0.5"
+            }
+        },
         "node_modules/junk": {
             "version": "3.1.0",
             "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz",
                 "shell-quote": "^1.8.1"
             }
         },
+        "node_modules/lazystream": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
+            "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "readable-stream": "^2.0.5"
+            },
+            "engines": {
+                "node": ">= 0.6.3"
+            }
+        },
         "node_modules/levn": {
             "version": "0.4.1",
             "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
             "dev": true,
             "license": "MIT"
         },
+        "node_modules/listenercount": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
+            "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==",
+            "dev": true,
+            "license": "ISC"
+        },
         "node_modules/loader-runner": {
             "version": "4.3.0",
             "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
             "dev": true,
             "license": "MIT"
         },
+        "node_modules/lodash.defaults": {
+            "version": "4.2.0",
+            "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
+            "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "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==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/lodash.escaperegexp": {
+            "version": "4.1.2",
+            "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
+            "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/lodash.flatten": {
+            "version": "4.4.0",
+            "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
+            "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/lodash.groupby": {
+            "version": "4.6.0",
+            "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz",
+            "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/lodash.isboolean": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+            "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/lodash.isequal": {
+            "version": "4.5.0",
+            "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+            "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
+            "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/lodash.isfunction": {
+            "version": "3.0.9",
+            "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz",
+            "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/lodash.isnil": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz",
+            "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/lodash.isplainobject": {
+            "version": "4.0.6",
+            "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+            "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+            "dev": true,
+            "license": "MIT"
+        },
+        "node_modules/lodash.isundefined": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz",
+            "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==",
+            "dev": true,
+            "license": "MIT"
+        },
         "node_modules/lodash.memoize": {
             "version": "4.1.2",
             "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
             "dev": true,
             "license": "MIT"
         },
+        "node_modules/lodash.union": {
+            "version": "4.6.0",
+            "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
+            "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
+            "dev": true,
+            "license": "MIT"
+        },
         "node_modules/lodash.uniq": {
             "version": "4.5.0",
             "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
             "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
             "dev": true,
             "license": "MIT",
-            "peer": true,
             "bin": {
                 "lz-string": "bin/bin.js"
             }
                 "node": ">=16 || 14 >=14.17"
             }
         },
+        "node_modules/mkdirp": {
+            "version": "0.5.6",
+            "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+            "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "minimist": "^1.2.6"
+            },
+            "bin": {
+                "mkdirp": "bin/cmd.js"
+            }
+        },
         "node_modules/moment": {
             "version": "2.30.1",
             "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
             "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
             "dev": true,
             "license": "MIT",
-            "peer": true,
             "dependencies": {
                 "any-promise": "^1.0.0",
                 "object-assign": "^4.0.1",
             "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
             "dev": true,
             "license": "MIT",
-            "peer": true,
             "engines": {
                 "node": ">= 6"
             }
             "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
             "dev": true,
             "license": "MIT",
-            "peer": true,
             "engines": {
                 "node": ">=0.10.0"
             }
             "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
             "dev": true,
             "license": "MIT",
-            "peer": true,
             "engines": {
                 "node": ">= 6"
             }
                 }
             ],
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "nanoid": "^3.3.11",
                 "picocolors": "^1.1.1",
             "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
             "dev": true,
             "license": "MIT",
-            "peer": true,
             "dependencies": {
                 "postcss-value-parser": "^4.0.0",
                 "read-cache": "^1.0.0",
             "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
             "dev": true,
             "license": "MIT",
-            "peer": true,
             "dependencies": {
                 "camelcase-css": "^2.0.1"
             },
                 }
             ],
             "license": "MIT",
-            "peer": true,
             "dependencies": {
                 "postcss-selector-parser": "^6.1.1"
             },
             "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
             "dev": true,
             "license": "MIT",
-            "peer": true,
             "dependencies": {
                 "ansi-regex": "^5.0.1",
                 "ansi-styles": "^5.0.0",
             "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
             "dev": true,
             "license": "MIT",
-            "peer": true,
             "engines": {
                 "node": ">=10"
             },
             "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
             "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
             "dev": true,
-            "license": "MIT",
-            "peer": true
+            "license": "MIT"
         },
         "node_modules/pretty-time": {
             "version": "1.1.0",
             "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
             "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
             "license": "MIT",
+            "peer": true,
             "engines": {
                 "node": ">=0.10.0"
             }
             "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
             "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "scheduler": "^0.26.0"
             },
             "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
             "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "@remix-run/router": "1.23.0",
                 "react-router": "6.30.1"
             "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
             "dev": true,
             "license": "MIT",
-            "peer": true,
             "dependencies": {
                 "pify": "^2.3.0"
             }
                 "safe-buffer": "~5.1.0"
             }
         },
+        "node_modules/readdir-glob": {
+            "version": "1.1.3",
+            "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
+            "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
+            "dev": true,
+            "license": "Apache-2.0",
+            "dependencies": {
+                "minimatch": "^5.1.0"
+            }
+        },
+        "node_modules/readdir-glob/node_modules/brace-expansion": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+            "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "balanced-match": "^1.0.0"
+            }
+        },
+        "node_modules/readdir-glob/node_modules/minimatch": {
+            "version": "5.1.6",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+            "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+            "dev": true,
+            "license": "ISC",
+            "dependencies": {
+                "brace-expansion": "^2.0.1"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
         "node_modules/readdirp": {
             "version": "3.6.0",
             "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
             "integrity": "sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA==",
             "dev": true,
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "@types/estree": "1.0.8"
             },
             "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
             "dev": true,
             "license": "MIT",
-            "peer": true,
             "dependencies": {
                 "@jridgewell/gen-mapping": "^0.3.2",
                 "commander": "^4.0.0",
             "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
             "dev": true,
             "license": "MIT",
-            "peer": true,
             "dependencies": {
                 "balanced-match": "^1.0.0"
             }
             "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
             "dev": true,
             "license": "MIT",
-            "peer": true,
             "engines": {
                 "node": ">= 6"
             }
             "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
             "dev": true,
             "license": "ISC",
-            "peer": true,
             "dependencies": {
                 "foreground-child": "^3.1.0",
                 "jackspeak": "^3.1.2",
             "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
             "dev": true,
             "license": "ISC",
-            "peer": true,
             "dependencies": {
                 "brace-expansion": "^2.0.1"
             },
             "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
             "dev": true,
             "license": "MIT",
-            "peer": true,
             "dependencies": {
                 "@alloc/quick-lru": "^5.2.0",
                 "arg": "^5.0.2",
             "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
             "dev": true,
             "license": "MIT",
-            "peer": true,
             "engines": {
                 "node": ">=14"
             },
                 }
             ],
             "license": "MIT",
-            "peer": true,
             "dependencies": {
                 "lilconfig": "^3.0.0",
                 "yaml": "^2.3.4"
                 "node": ">=6"
             }
         },
+        "node_modules/tar-stream": {
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+            "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "bl": "^4.0.3",
+                "end-of-stream": "^1.4.1",
+                "fs-constants": "^1.0.0",
+                "inherits": "^2.0.3",
+                "readable-stream": "^3.1.1"
+            },
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/tar-stream/node_modules/readable-stream": {
+            "version": "3.6.2",
+            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+            "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "inherits": "^2.0.3",
+                "string_decoder": "^1.1.1",
+                "util-deprecate": "^1.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
         "node_modules/terser": {
             "version": "5.43.1",
             "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
             "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
             "dev": true,
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "fast-deep-equal": "^3.1.3",
                 "fast-uri": "^3.0.1",
             "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
             "dev": true,
             "license": "MIT",
-            "peer": true,
             "dependencies": {
                 "any-promise": "^1.0.0"
             }
             "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
             "dev": true,
             "license": "MIT",
-            "peer": true,
             "dependencies": {
                 "thenify": ">= 3.1.0 < 4"
             },
             "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
             "dev": true,
             "license": "MIT",
+            "peer": true,
             "engines": {
                 "node": ">=12"
             },
             "dev": true,
             "license": "MIT"
         },
+        "node_modules/tmp": {
+            "version": "0.2.5",
+            "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
+            "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
+            "dev": true,
+            "license": "MIT",
+            "engines": {
+                "node": ">=14.14"
+            }
+        },
         "node_modules/to-arraybuffer": {
             "version": "1.0.1",
             "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
                 "node": ">=6"
             }
         },
+        "node_modules/traverse": {
+            "version": "0.3.9",
+            "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
+            "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==",
+            "dev": true,
+            "license": "MIT/X11",
+            "engines": {
+                "node": "*"
+            }
+        },
         "node_modules/ts-interface-checker": {
             "version": "0.1.13",
             "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
             "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
             "dev": true,
-            "license": "Apache-2.0",
-            "peer": true
+            "license": "Apache-2.0"
         },
         "node_modules/tsconfig-paths": {
             "version": "3.15.0",
                 "node": ">= 0.8"
             }
         },
+        "node_modules/unzipper": {
+            "version": "0.10.14",
+            "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz",
+            "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "big-integer": "^1.6.17",
+                "binary": "~0.3.0",
+                "bluebird": "~3.4.1",
+                "buffer-indexof-polyfill": "~1.0.0",
+                "duplexer2": "~0.1.4",
+                "fstream": "^1.0.12",
+                "graceful-fs": "^4.2.2",
+                "listenercount": "~1.0.1",
+                "readable-stream": "~2.3.6",
+                "setimmediate": "~1.0.4"
+            }
+        },
         "node_modules/update-browserslist-db": {
             "version": "1.1.3",
             "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
             "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
             "dev": true,
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "esbuild": "^0.25.0",
                 "fdir": "^6.4.4",
             "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
             "dev": true,
             "license": "MIT",
+            "peer": true,
             "engines": {
                 "node": ">=12"
             },
             "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
             "dev": true,
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "@types/chai": "^5.2.2",
                 "@vitest/expect": "3.2.4",
             "integrity": "sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==",
             "dev": true,
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "@types/eslint-scope": "^3.7.7",
                 "@types/estree": "^1.0.6",
             "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==",
             "dev": true,
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "@discoveryjs/json-ext": "^0.5.0",
                 "@webpack-cli/configtest": "^1.2.0",
             "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
             "dev": true,
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "fast-deep-equal": "^3.1.3",
                 "fast-uri": "^3.0.1",
             "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
             "dev": true,
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "fast-deep-equal": "^3.1.3",
                 "fast-uri": "^3.0.1",
             "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
             "dev": true,
             "license": "MIT",
+            "peer": true,
             "dependencies": {
                 "fast-deep-equal": "^3.1.3",
                 "fast-uri": "^3.0.1",
             "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
             "dev": true,
             "license": "ISC",
-            "peer": true,
             "bin": {
                 "yaml": "bin.mjs"
             },
                 "toposort": "^2.0.2",
                 "type-fest": "^2.19.0"
             }
+        },
+        "node_modules/zip-stream": {
+            "version": "4.1.1",
+            "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz",
+            "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "archiver-utils": "^3.0.4",
+                "compress-commons": "^4.1.2",
+                "readable-stream": "^3.6.0"
+            },
+            "engines": {
+                "node": ">= 10"
+            }
+        },
+        "node_modules/zip-stream/node_modules/archiver-utils": {
+            "version": "3.0.4",
+            "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz",
+            "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "glob": "^7.2.3",
+                "graceful-fs": "^4.2.0",
+                "lazystream": "^1.0.0",
+                "lodash.defaults": "^4.2.0",
+                "lodash.difference": "^4.5.0",
+                "lodash.flatten": "^4.4.0",
+                "lodash.isplainobject": "^4.0.6",
+                "lodash.union": "^4.6.0",
+                "normalize-path": "^3.0.0",
+                "readable-stream": "^3.6.0"
+            },
+            "engines": {
+                "node": ">= 10"
+            }
+        },
+        "node_modules/zip-stream/node_modules/readable-stream": {
+            "version": "3.6.2",
+            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+            "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "inherits": "^2.0.3",
+                "string_decoder": "^1.1.1",
+                "util-deprecate": "^1.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
         }
     }
 }
index bf2c41121c4b64f32b1f9b1be67240f638f28edd..e60775d2d071a92701570b40122fa4f6f135f230 100644 (file)
@@ -24,6 +24,7 @@
         "eslint": "^9.29.0",
         "eslint-plugin-import": "^2.25.4",
         "eslint-plugin-react": "^7.29.3",
+        "exceljs": "^4.4.0",
         "globals": "^16.2.0",
         "i18next": "^25.2.1",
         "jsdom": "^26.1.0",
index e5dbb9d23ef6a36fee977534fa181a388b85b1c3..5ffd927a7e1a652007a16802de9ea308d374031b 100644 (file)
@@ -63,6 +63,7 @@ Icon.DISCORD = makePreset('DiscordIcon', ['fab', 'discord']);
 Icon.DOWNLOAD = makePreset('DownloadIcon', 'download');
 Icon.EDIT = makePreset('EditIcon', 'edit');
 Icon.ERROR = makePreset('ErrorIcon', 'triangle-exclamation');
+Icon.EXCEL = makePreset('ExcelIcon', 'file-excel');
 Icon.FILTER = makePreset('FilterIcon', 'filter');
 Icon.FINISHED = makePreset('FinishedIcon', 'square-check');
 Icon.FIRST_PLACE = makePreset('FirstPlaceIcon', 'trophy');
@@ -71,6 +72,7 @@ Icon.FORFEIT = makePreset('ForfeitIcon', 'square-xmark');
 Icon.HASH = makePreset('HashIcon', 'hashtag');
 Icon.INFO = makePreset('Info', 'circle-info');
 Icon.INVERT = makePreset('InvertIcon', 'circle-half-stroke');
+Icon.JS = makePreset('JsIcon', ['fab', 'js-square']);
 Icon.LANGUAGE = makePreset('LanguageIcon', 'language');
 Icon.LOAD = makePreset('LoadIcon', 'upload');
 Icon.LOADING = makePreset('LoadingIcon', 'spinner');
index 241c2f02dfec817001c8c4a4b37efed2ab63ef42..281a2455c4d786840046a2a11586211124c5a45d 100644 (file)
@@ -4,6 +4,7 @@ import { Button, Col, Container, Row } from 'react-bootstrap';
 import { useTranslation } from 'react-i18next';
 
 import ApplyButton from './ApplyButton';
+import ExportButton from './ExportButton';
 import GroupInterface from './GroupInterface';
 import Scoreboard from './Scoreboard';
 import ScoreChartButton from './ScoreChartButton';
@@ -17,6 +18,7 @@ import Box from '../users/Box';
 import {
        isRunner,
        mayAddRounds,
+       mayExportTournament,
        mayUpdateTournament,
        mayViewProtocol,
 } from '../../helpers/permissions';
@@ -78,6 +80,9 @@ const Detail = ({
                                                {mayUpdateTournament(user, tournament) ?
                                                        <SettingsButton tournament={tournament} />
                                                : null}
+                                               {mayExportTournament(user, tournament) ?
+                                                       <ExportButton tournament={tournament} />
+                                               : null}
                                                {mayViewProtocol(user, tournament) ?
                                                        <Protocol id={tournament.id} />
                                                : null}
diff --git a/resources/js/components/tournament/ExportButton.jsx b/resources/js/components/tournament/ExportButton.jsx
new file mode 100644 (file)
index 0000000..b0c796f
--- /dev/null
@@ -0,0 +1,68 @@
+import PropTypes from 'prop-types';
+import React, { useState } from 'react';
+import { Button } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+import toastr from 'toastr';
+
+import Icon from '../common/Icon';
+import { exportJson, exportXlsx } from '../../helpers/Tournament';
+
+const ExportButton = ({ tournament }) => {
+       const [processing, setProcessing] = useState(false);
+
+       const { t } = useTranslation();
+
+       const handleJson = React.useCallback(async () => {
+               setProcessing(true);
+               try {
+                       await exportJson(tournament);
+               } catch (e) {
+                       toastr.error(t('general.exportError', e))
+               }
+               setProcessing(false);
+       }, [t, tournament]);
+
+       const handleXlsx = React.useCallback(async () => {
+               setProcessing(true);
+               try {
+                       await exportXlsx(tournament);
+               } catch (e) {
+                       toastr.error(t('general.exportError', e))
+               }
+               setProcessing(false);
+       }, [t, tournament]);
+
+       return <>
+               <Button
+                       disabled={processing}
+                       onClick={handleXlsx}
+                       title={t('button.exportExcel')}
+                       variant="outline-secondary"
+               >
+                       {processing ? (
+                               <Icon.LOADING title="" />
+                       ) : (
+                               <Icon.EXCEL title="" />
+                       )}
+               </Button>
+               <Button
+                       disabled={processing}
+                       onClick={handleJson}
+                       title={t('button.exportJs')}
+                       variant="outline-secondary"
+               >
+                       {processing ? (
+                               <Icon.LOADING title="" />
+                       ) : (
+                               <Icon.JS title="" />
+                       )}
+               </Button>
+       </>;
+};
+
+ExportButton.propTypes = {
+       tournament: PropTypes.shape({
+       }),
+};
+
+export default ExportButton;
index d1f5ef335785c14d6b417e601de6fdaff6941f7e..dd349de7f5d7364af0de51a5ba744529e8054870 100644 (file)
@@ -1,9 +1,13 @@
 import Participant from './Participant';
 import Tournament from './Tournament';
 
-export const formatNumber = (tournament, round) => {
+export const formatNumberAlways = (tournament, round) => {
        const group = (tournament?.group_size > 1 && round?.group) || '';
-       return tournament.show_numbers && round?.number ? `#${round.number}${group} ` : '';
+       return round?.number ? `#${round.number}${group} ` : `X${round.id}`;
+};
+
+export const formatNumber = (tournament, round) => {
+       return tournament.show_numbers ? formatNumberAlways(tournament, round) : '';
 };
 
 export const hasResults = (round) => {
@@ -38,6 +42,8 @@ export const patchResult = (round, result) => {
 };
 
 export default {
+       formatNumber,
+       formatNumberAlways,
        hasResults,
        isComplete,
        patchResult,
index 6b77588113420b5b6825e1d9c5253f9c144ecff1..a96ab3649f076c96d0732c26c25a654f1acd40e8 100644 (file)
@@ -1,6 +1,12 @@
+import axios from 'axios';
+
 import Application from './Application';
+import downloadBlob from './downloadBlob';
 import Participant from './Participant';
+import Result from './Result';
 import Round from './Round';
+import User from './User';
+import i18n from '../i18n';
 
 export const compareScore = (a, b) => {
        const a_score = a && a.score ? a.score : 0;
@@ -10,6 +16,133 @@ export const compareScore = (a, b) => {
        return Participant.compareUsername(a.participant, b.participant) * -1;
 };
 
+export const getLastRound = tournament => {
+       if (!tournament || !tournament.rounds || !tournament.rounds.length) return null;
+       return tournament.rounds.slice(-1)[0];
+};
+
+export const canLoadMoreRounds = tournament => {
+       const last_round = getLastRound(tournament);
+       return last_round && last_round.number > 1;
+};
+
+export const loadAllRounds = async (tournament) => {
+       if (canLoadMoreRounds(tournament)) {
+               const last_round = getLastRound(tournament);
+               if (!last_round) return tournament;
+               const last_known = last_round.number;
+               const rsp = await axios.get(
+                       `/api/tournaments/${tournament.id}/more-rounds`,
+                       { params: { last_known } },
+               );
+               return loadAllRounds({
+                       ...tournament,
+                       rounds: [...tournament.rounds, ...rsp.data],
+               });
+       }
+       return tournament;
+};
+
+export const exportJson = async (tnmt) => {
+       const tournament = await loadAllRounds(tnmt);
+       const blob = new Blob(
+               [JSON.stringify(tournament, null, 4)],
+               { type: 'application/json' },
+       );
+       downloadBlob(blob, `${tournament.title}.json`);
+};
+
+const collectAllUsers = (tournament) => {
+       const users = [];
+       tournament.rounds.forEach((round) => {
+               round.results.forEach((result) => {
+                       if (!users.find((user) => user.id === result.user.id)) {
+                               users.push(result.user);
+                       }
+               });
+       })
+       return users;
+};
+
+const getRoundWorksheetTitle = (tournament, round) => {
+       const title = round.title
+               ? `${Round.formatNumberAlways(tournament, round)} ${round.title}`
+               : Round.formatNumberAlways(tournament, round);
+       return title.replace(/[*?:\\/[\]]/g, '-').substr(0, 31);
+};
+
+export const exportXlsx = async (tnmt) => {
+       const ExcelJS = (await import('exceljs')).default;
+       const tournament = await loadAllRounds(tnmt);
+       const workbook = new ExcelJS.Workbook();
+       workbook.creator = i18n.t('general.appName');
+       workbook.created = new Date(tournament.created_at);
+       workbook.modified = new Date(tournament.updated_at);
+
+       const users = collectAllUsers(tournament);
+       users.sort(User.compareUsername);
+       const summary = workbook.addWorksheet(i18n.t('general.summary'), {
+               views: [{ state: 'frozen', xSplit: 1, ySplit: 1 }],
+       });
+       if (users.length > tournament.rounds.length) {
+               summary.addRow([
+                       i18n.t('results.runner'),
+                       ...tournament.rounds.map((round) => round.title || Round.formatNumberAlways(tournament, round)),
+               ]);
+               users.forEach((user) => {
+                       summary.addRow([
+                               User.getUserName(user),
+                               ...tournament.rounds.map((round) => Result.getTime(User.findResult(user, round), true)),
+                       ]);
+               });
+       } else {
+               summary.addRow([
+                       i18n.t('results.round'),
+                       ...users.map(User.getUserName),
+               ]);
+               tournament.rounds.forEach((round) => {
+                       summary.addRow([
+                               round.title || Round.formatNumberAlways(tournament, round),
+                               ...users.map((user) => Result.getTime(User.findResult(user, round), true)),
+                       ]);
+               });
+       }
+
+       tournament.rounds.forEach((round) => {
+               const worksheet = workbook.addWorksheet(getRoundWorksheetTitle(tournament, round), {
+                       views: [{ state: 'frozen', xSplit: 1, ySplit: 1 }],
+               });
+               worksheet.addRow([
+                       i18n.t('results.runner'),
+                       i18n.t('results.forfeit'),
+                       i18n.t('results.reportTime'),
+                       i18n.t('results.placement'),
+                       i18n.t('results.score'),
+                       i18n.t('results.comment'),
+                       i18n.t('results.vod'),
+                       i18n.t('general.created_at'),
+               ]);
+               round.results.forEach((result) => {
+                       worksheet.addRow([
+                               User.getUserName(result.user),
+                               result.forfeit ? 'x' : '-',
+                               result.time,
+                               result.placement,
+                               result.score,
+                               result.comment || '',
+                               result.vod || '',
+                               result.created_at,
+                       ]);
+               });
+       });
+       const buffer = await workbook.xlsx.writeBuffer();
+       const blob = new Blob(
+               [buffer.buffer],
+               { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
+       );
+       downloadBlob(blob, `${tournament.title}.xlsx`);
+};
+
 export const findParticipant = (tournament, user) => {
        if (!tournament || !tournament.participants || !tournament.participants.length) return null;
        if (!user || !user.id) return null;
@@ -30,16 +163,6 @@ export const getRunners = tournament => {
                .sort(Participant.compareUsername);
 };
 
-export const getLastRound = tournament => {
-       if (!tournament || !tournament.rounds || !tournament.rounds.length) return null;
-       return tournament.rounds.slice(-1)[0];
-};
-
-export const canLoadMoreRounds = tournament => {
-       const last_round = getLastRound(tournament);
-       return last_round && last_round.number > 1;
-};
-
 export const hasAssignedGroups = tournament => (tournament?.type === 'open-grouped-async');
 
 export const hasScoreboard = tournament => !!(tournament && tournament.type === 'signup-async');
diff --git a/resources/js/helpers/downloadBlob.js b/resources/js/helpers/downloadBlob.js
new file mode 100644 (file)
index 0000000..f2de0b6
--- /dev/null
@@ -0,0 +1,10 @@
+const downloadBlob = (blob, filename) => {
+       const url = URL.createObjectURL(blob);
+       const a = document.createElement('a');
+       a.href = url;
+       a.download = filename;
+       a.click();
+       return a;
+};
+
+export default downloadBlob;
index 6a4ca0de141a5c08f03838bba2b3bf2b4175e1ca..d1d1952e407a259de0957c5527e2841e44816196 100644 (file)
@@ -177,6 +177,9 @@ export const maySetSeed = (user, tournament, round) =>
        !round.locked &&
                (isRunner(user, tournament) || isTournamentAdmin(user, tournament));
 
+export const mayExportTournament = (user, tournament) =>
+       isAdmin(user) || isTournamentAdmin(user, tournament);
+
 export const mayUpdateTournament = (user, tournament) =>
        isAdmin(user) || isTournamentAdmin(user, tournament);
 
index 1ddeee42409720af41d81f91c023eeea458f302b..5497a0b73ea5808a9048441c8bd9aa9a0e6de064 100644 (file)
@@ -79,6 +79,8 @@ export default {
                        confirm: 'Bestätigen',
                        delete: 'Löschen',
                        edit: 'Bearbeiten',
+                       exportExcel: 'XLSX Exportieren',
+                       exportJs: 'JSON Exportieren',
                        filter: 'Filter',
                        generate: 'Generieren',
                        help: 'Hilfe',
@@ -356,6 +358,8 @@ export default {
                        appDescription: 'Turniere und Tutorials für The Legend of Zelda: A Link to the Past Randomizer',
                        appName: 'ALttP',
                        copied: 'Kopiert',
+                       created_at: 'Erstellt am',
+                       exportError: 'Fehler beim Exportieren',
                        languages: {
                                de: 'Deutsch',
                                en: 'Englisch',
@@ -370,6 +374,7 @@ export default {
                        resetSuccess: 'Zurückgesetzt',
                        saveError: 'Fehler beim Speichern',
                        saveSuccess: 'Gespeichert',
+                       summary: 'Zusammenfassung',
                        upload: 'Datei hochladen',
                        uploadError: 'Fehler beim Hochladen',
                        uploading: 'Am Hochladen...',
@@ -611,6 +616,7 @@ export default {
                        result: 'Ergebnis',
                        round: 'Runde',
                        runner: 'Runner',
+                       score: 'Punkte',
                        time: 'Zeit: {{ time }}',
                        vod: 'VoD',
                        vodNote: 'Falls ihr euer VoD teilen wollte, gerne hier rein.',
index c3d4268071ff7fdd93d84faf66815ebaf1b3370e..a3a3c3fa1b2ede9eb2dbcf66b2a6e18a44e371c1 100644 (file)
@@ -79,6 +79,8 @@ export default {
                        confirm: 'Confirm',
                        delete: 'Delete',
                        edit: 'Edit',
+                       exportExcel: 'Export XLSX',
+                       exportJs: 'Export JSON',
                        filter: 'Filter',
                        generate: 'Generate',
                        help: 'Help',
@@ -356,6 +358,8 @@ export default {
                        appDescription: 'Tournaments and tutorials for The Legend of Zelda: A Link to the Past Randomizer',
                        appName: 'ALttP',
                        copied: 'Copied',
+                       created_at: 'Created at',
+                       exportError: 'Error during export',
                        languages: {
                                de: 'German',
                                en: 'English',
@@ -370,6 +374,7 @@ export default {
                        resetSuccess: 'Reset successful',
                        saveError: 'Error saving',
                        saveSuccess: 'Saved successfully',
+                       summary: 'Summary',
                        upload: 'Upload file',
                        uploadError: 'Error uploading',
                        uploading: 'Uploading...',
@@ -611,6 +616,7 @@ export default {
                        result: 'Result',
                        round: 'Round',
                        runner: 'Runner',
+                       score: 'Score',
                        time: 'Time: {{ time }}',
                        vod: 'VoD',
                        vodNote: 'If you want to share your VoD, go ahead.',