diff --git a/Dockerfile b/Dockerfile
index 474988fe7a2eacbea6dcebdaaf59f6937902430c..a9dccf5ee222d7e84f87053d48867f5907bf4478 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,7 @@
 FROM node:21-alpine
 
+RUN apk update && apk add build-base g++ cairo-dev pango-dev giflib-dev
+
 WORKDIR /app
 COPY package*.json ./
 COPY .env ./
diff --git a/docker-compose.yml b/docker-compose.yml
index 1c4033c9143cb80a2bbc4329320f9056803ca98b..b9a3f0b927c33ed82492d87a9594aaac9facc020 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,7 +2,9 @@ version: 3.8
 
 services:
   web:
-    build: .
+    build:
+      context: .
+      dockerfile: Dockerfile
     ports:
       - 3000:3000
     restart: always
diff --git a/package-lock.json b/package-lock.json
index 59a2e42e3e19df57ac018ab45ec5f85d1a3578da..59fd83bf5aa83e036d824a358515ba833e1c5e44 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1369,9 +1369,9 @@
       }
     },
     "node_modules/@rollup/rollup-android-arm-eabi": {
-      "version": "4.21.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.0.tgz",
-      "integrity": "sha512-WTWD8PfoSAJ+qL87lE7votj3syLavxunWhzCnx3XFxFiI/BA/r3X7MUM8dVrH8rb2r4AiO8jJsr3ZjdaftmnfA==",
+      "version": "4.21.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.2.tgz",
+      "integrity": "sha512-fSuPrt0ZO8uXeS+xP3b+yYTCBUd05MoSp2N/MFOgjhhUhMmchXlpTQrTpI8T+YAwAQuK7MafsCOxW7VrPMrJcg==",
       "cpu": [
         "arm"
       ],
@@ -1383,9 +1383,9 @@
       ]
     },
     "node_modules/@rollup/rollup-android-arm64": {
-      "version": "4.21.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.0.tgz",
-      "integrity": "sha512-a1sR2zSK1B4eYkiZu17ZUZhmUQcKjk2/j9Me2IDjk1GHW7LB5Z35LEzj9iJch6gtUfsnvZs1ZNyDW2oZSThrkA==",
+      "version": "4.21.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.2.tgz",
+      "integrity": "sha512-xGU5ZQmPlsjQS6tzTTGwMsnKUtu0WVbl0hYpTPauvbRAnmIvpInhJtgjj3mcuJpEiuUw4v1s4BimkdfDWlh7gA==",
       "cpu": [
         "arm64"
       ],
@@ -1397,9 +1397,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-arm64": {
-      "version": "4.21.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.0.tgz",
-      "integrity": "sha512-zOnKWLgDld/svhKO5PD9ozmL6roy5OQ5T4ThvdYZLpiOhEGY+dp2NwUmxK0Ld91LrbjrvtNAE0ERBwjqhZTRAA==",
+      "version": "4.21.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.2.tgz",
+      "integrity": "sha512-99AhQ3/ZMxU7jw34Sq8brzXqWH/bMnf7ZVhvLk9QU2cOepbQSVTns6qoErJmSiAvU3InRqC2RRZ5ovh1KN0d0Q==",
       "cpu": [
         "arm64"
       ],
@@ -1411,9 +1411,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-x64": {
-      "version": "4.21.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.0.tgz",
-      "integrity": "sha512-7doS8br0xAkg48SKE2QNtMSFPFUlRdw9+votl27MvT46vo44ATBmdZdGysOevNELmZlfd+NEa0UYOA8f01WSrg==",
+      "version": "4.21.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.2.tgz",
+      "integrity": "sha512-ZbRaUvw2iN/y37x6dY50D8m2BnDbBjlnMPotDi/qITMJ4sIxNY33HArjikDyakhSv0+ybdUxhWxE6kTI4oX26w==",
       "cpu": [
         "x64"
       ],
@@ -1425,9 +1425,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
-      "version": "4.21.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.0.tgz",
-      "integrity": "sha512-pWJsfQjNWNGsoCq53KjMtwdJDmh/6NubwQcz52aEwLEuvx08bzcy6tOUuawAOncPnxz/3siRtd8hiQ32G1y8VA==",
+      "version": "4.21.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.2.tgz",
+      "integrity": "sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w==",
       "cpu": [
         "arm"
       ],
@@ -1439,9 +1439,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm-musleabihf": {
-      "version": "4.21.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.0.tgz",
-      "integrity": "sha512-efRIANsz3UHZrnZXuEvxS9LoCOWMGD1rweciD6uJQIx2myN3a8Im1FafZBzh7zk1RJ6oKcR16dU3UPldaKd83w==",
+      "version": "4.21.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.2.tgz",
+      "integrity": "sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w==",
       "cpu": [
         "arm"
       ],
@@ -1453,9 +1453,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-gnu": {
-      "version": "4.21.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.0.tgz",
-      "integrity": "sha512-ZrPhydkTVhyeGTW94WJ8pnl1uroqVHM3j3hjdquwAcWnmivjAwOYjTEAuEDeJvGX7xv3Z9GAvrBkEzCgHq9U1w==",
+      "version": "4.21.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.2.tgz",
+      "integrity": "sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw==",
       "cpu": [
         "arm64"
       ],
@@ -1467,9 +1467,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-musl": {
-      "version": "4.21.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.0.tgz",
-      "integrity": "sha512-cfaupqd+UEFeURmqNP2eEvXqgbSox/LHOyN9/d2pSdV8xTrjdg3NgOFJCtc1vQ/jEke1qD0IejbBfxleBPHnPw==",
+      "version": "4.21.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.2.tgz",
+      "integrity": "sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w==",
       "cpu": [
         "arm64"
       ],
@@ -1481,9 +1481,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
-      "version": "4.21.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.0.tgz",
-      "integrity": "sha512-ZKPan1/RvAhrUylwBXC9t7B2hXdpb/ufeu22pG2psV7RN8roOfGurEghw1ySmX/CmDDHNTDDjY3lo9hRlgtaHg==",
+      "version": "4.21.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.2.tgz",
+      "integrity": "sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ==",
       "cpu": [
         "ppc64"
       ],
@@ -1495,9 +1495,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-riscv64-gnu": {
-      "version": "4.21.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.0.tgz",
-      "integrity": "sha512-H1eRaCwd5E8eS8leiS+o/NqMdljkcb1d6r2h4fKSsCXQilLKArq6WS7XBLDu80Yz+nMqHVFDquwcVrQmGr28rg==",
+      "version": "4.21.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.2.tgz",
+      "integrity": "sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg==",
       "cpu": [
         "riscv64"
       ],
@@ -1509,9 +1509,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-s390x-gnu": {
-      "version": "4.21.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.0.tgz",
-      "integrity": "sha512-zJ4hA+3b5tu8u7L58CCSI0A9N1vkfwPhWd/puGXwtZlsB5bTkwDNW/+JCU84+3QYmKpLi+XvHdmrlwUwDA6kqw==",
+      "version": "4.21.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.2.tgz",
+      "integrity": "sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ==",
       "cpu": [
         "s390x"
       ],
@@ -1523,9 +1523,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-gnu": {
-      "version": "4.21.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.0.tgz",
-      "integrity": "sha512-e2hrvElFIh6kW/UNBQK/kzqMNY5mO+67YtEh9OA65RM5IJXYTWiXjX6fjIiPaqOkBthYF1EqgiZ6OXKcQsM0hg==",
+      "version": "4.21.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.2.tgz",
+      "integrity": "sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==",
       "cpu": [
         "x64"
       ],
@@ -1537,9 +1537,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-musl": {
-      "version": "4.21.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.0.tgz",
-      "integrity": "sha512-1vvmgDdUSebVGXWX2lIcgRebqfQSff0hMEkLJyakQ9JQUbLDkEaMsPTLOmyccyC6IJ/l3FZuJbmrBw/u0A0uCQ==",
+      "version": "4.21.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.2.tgz",
+      "integrity": "sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg==",
       "cpu": [
         "x64"
       ],
@@ -1551,9 +1551,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-arm64-msvc": {
-      "version": "4.21.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.0.tgz",
-      "integrity": "sha512-s5oFkZ/hFcrlAyBTONFY1TWndfyre1wOMwU+6KCpm/iatybvrRgmZVM+vCFwxmC5ZhdlgfE0N4XorsDpi7/4XQ==",
+      "version": "4.21.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.2.tgz",
+      "integrity": "sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ==",
       "cpu": [
         "arm64"
       ],
@@ -1565,9 +1565,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-ia32-msvc": {
-      "version": "4.21.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.0.tgz",
-      "integrity": "sha512-G9+TEqRnAA6nbpqyUqgTiopmnfgnMkR3kMukFBDsiyy23LZvUCpiUwjTRx6ezYCjJODXrh52rBR9oXvm+Fp5wg==",
+      "version": "4.21.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.2.tgz",
+      "integrity": "sha512-5rA4vjlqgrpbFVVHX3qkrCo/fZTj1q0Xxpg+Z7yIo3J2AilW7t2+n6Q8Jrx+4MrYpAnjttTYF8rr7bP46BPzRw==",
       "cpu": [
         "ia32"
       ],
@@ -1579,9 +1579,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-x64-msvc": {
-      "version": "4.21.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.0.tgz",
-      "integrity": "sha512-2jsCDZwtQvRhejHLfZ1JY6w6kEuEtfF9nzYsZxzSlNVKDX+DpsDJ+Rbjkm74nvg2rdx0gwBS+IMdvwJuq3S9pQ==",
+      "version": "4.21.2",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.2.tgz",
+      "integrity": "sha512-6UUxd0+SKomjdzuAcp+HAmxw1FlGBnl1v2yEPSabtx4lBfdXHDVsW7+lQkgz9cNFJGY3AWR7+V8P5BqkD9L9nA==",
       "cpu": [
         "x64"
       ],
@@ -1622,9 +1622,9 @@
       }
     },
     "node_modules/@sveltejs/kit": {
-      "version": "2.5.24",
-      "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.24.tgz",
-      "integrity": "sha512-Nr2oxsCsDfEkdS/zzQQQbsPYTbu692Qs3/iE3L7VHzCVjG2+WujF9oMUozWI7GuX98KxYSoPMlAsfmDLSg44hQ==",
+      "version": "2.5.25",
+      "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.25.tgz",
+      "integrity": "sha512-5hBSEN8XEjDZ5+2bHkFh8Z0QyOk0C187cyb12aANe1c8aeKbfu5ZD5XaC2vEH4h0alJFDXPdUkXQBmeeXeMr1A==",
       "dev": true,
       "hasInstallScript": true,
       "license": "MIT",
@@ -1655,9 +1655,9 @@
       }
     },
     "node_modules/@sveltejs/vite-plugin-svelte": {
-      "version": "4.0.0-next.6",
-      "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.0-next.6.tgz",
-      "integrity": "sha512-7+bEFN5F9pthG6nOEHNz9yioHxNXK6yl+0GnTy9WOfxN/SvPykkH/Hs6MqTGjo47a9G2q3QXQnzuxG5WXNX4Tg==",
+      "version": "4.0.0-next.7",
+      "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.0-next.7.tgz",
+      "integrity": "sha512-yMUnAqquoayvBDztk1rWUgdtvjv7YcHgopCAB7sWl9SQht8U/7lqwTlJU0ZTAY09pFFRe6bbakd7YoiyyIvJiA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -1666,7 +1666,7 @@
         "deepmerge": "^4.3.1",
         "kleur": "^4.1.5",
         "magic-string": "^0.30.11",
-        "vitefu": "^0.2.5"
+        "vitefu": "^1.0.2"
       },
       "engines": {
         "node": "^18.0.0 || ^20.0.0 || >=22"
@@ -1711,9 +1711,9 @@
       "license": "MIT"
     },
     "node_modules/@types/eslint": {
-      "version": "9.6.0",
-      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.0.tgz",
-      "integrity": "sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg==",
+      "version": "9.6.1",
+      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
+      "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -1784,9 +1784,9 @@
       "license": "MIT"
     },
     "node_modules/@types/node": {
-      "version": "22.5.0",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz",
-      "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==",
+      "version": "22.5.3",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.3.tgz",
+      "integrity": "sha512-njripolh85IA9SQGTAqbmnNZTdxv7X/4OYGPz8tgy5JDr8MP+uDBa921GpYEoDDnwm0Hmn5ZPeJgiiSTPoOzkQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -1835,17 +1835,17 @@
       "license": "MIT"
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "8.2.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.2.0.tgz",
-      "integrity": "sha512-02tJIs655em7fvt9gps/+4k4OsKULYGtLBPJfOsmOq1+3cdClYiF0+d6mHu6qDnTcg88wJBkcPLpQhq7FyDz0A==",
+      "version": "8.4.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.4.0.tgz",
+      "integrity": "sha512-rg8LGdv7ri3oAlenMACk9e+AR4wUV0yrrG+XKsGKOK0EVgeEDqurkXMPILG2836fW4ibokTB5v4b6Z9+GYQDEw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@eslint-community/regexpp": "^4.10.0",
-        "@typescript-eslint/scope-manager": "8.2.0",
-        "@typescript-eslint/type-utils": "8.2.0",
-        "@typescript-eslint/utils": "8.2.0",
-        "@typescript-eslint/visitor-keys": "8.2.0",
+        "@typescript-eslint/scope-manager": "8.4.0",
+        "@typescript-eslint/type-utils": "8.4.0",
+        "@typescript-eslint/utils": "8.4.0",
+        "@typescript-eslint/visitor-keys": "8.4.0",
         "graphemer": "^1.4.0",
         "ignore": "^5.3.1",
         "natural-compare": "^1.4.0",
@@ -1869,16 +1869,16 @@
       }
     },
     "node_modules/@typescript-eslint/parser": {
-      "version": "8.2.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.2.0.tgz",
-      "integrity": "sha512-j3Di+o0lHgPrb7FxL3fdEy6LJ/j2NE8u+AP/5cQ9SKb+JLH6V6UHDqJ+e0hXBkHP1wn1YDFjYCS9LBQsZDlDEg==",
+      "version": "8.4.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.4.0.tgz",
+      "integrity": "sha512-NHgWmKSgJk5K9N16GIhQ4jSobBoJwrmURaLErad0qlLjrpP5bECYg+wxVTGlGZmJbU03jj/dfnb6V9bw+5icsA==",
       "dev": true,
       "license": "BSD-2-Clause",
       "dependencies": {
-        "@typescript-eslint/scope-manager": "8.2.0",
-        "@typescript-eslint/types": "8.2.0",
-        "@typescript-eslint/typescript-estree": "8.2.0",
-        "@typescript-eslint/visitor-keys": "8.2.0",
+        "@typescript-eslint/scope-manager": "8.4.0",
+        "@typescript-eslint/types": "8.4.0",
+        "@typescript-eslint/typescript-estree": "8.4.0",
+        "@typescript-eslint/visitor-keys": "8.4.0",
         "debug": "^4.3.4"
       },
       "engines": {
@@ -1898,14 +1898,14 @@
       }
     },
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "8.2.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.2.0.tgz",
-      "integrity": "sha512-OFn80B38yD6WwpoHU2Tz/fTz7CgFqInllBoC3WP+/jLbTb4gGPTy9HBSTsbDWkMdN55XlVU0mMDYAtgvlUspGw==",
+      "version": "8.4.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.4.0.tgz",
+      "integrity": "sha512-n2jFxLeY0JmKfUqy3P70rs6vdoPjHK8P/w+zJcV3fk0b0BwRXC/zxRTEnAsgYT7MwdQDt/ZEbtdzdVC+hcpF0A==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.2.0",
-        "@typescript-eslint/visitor-keys": "8.2.0"
+        "@typescript-eslint/types": "8.4.0",
+        "@typescript-eslint/visitor-keys": "8.4.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1916,14 +1916,14 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "8.2.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.2.0.tgz",
-      "integrity": "sha512-g1CfXGFMQdT5S+0PSO0fvGXUaiSkl73U1n9LTK5aRAFnPlJ8dLKkXr4AaLFvPedW8lVDoMgLLE3JN98ZZfsj0w==",
+      "version": "8.4.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.4.0.tgz",
+      "integrity": "sha512-pu2PAmNrl9KX6TtirVOrbLPLwDmASpZhK/XU7WvoKoCUkdtq9zF7qQ7gna0GBZFN0hci0vHaSusiL2WpsQk37A==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "8.2.0",
-        "@typescript-eslint/utils": "8.2.0",
+        "@typescript-eslint/typescript-estree": "8.4.0",
+        "@typescript-eslint/utils": "8.4.0",
         "debug": "^4.3.4",
         "ts-api-utils": "^1.3.0"
       },
@@ -1941,9 +1941,9 @@
       }
     },
     "node_modules/@typescript-eslint/types": {
-      "version": "8.2.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.2.0.tgz",
-      "integrity": "sha512-6a9QSK396YqmiBKPkJtxsgZZZVjYQ6wQ/TlI0C65z7vInaETuC6HAHD98AGLC8DyIPqHytvNuS8bBVvNLKyqvQ==",
+      "version": "8.4.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.4.0.tgz",
+      "integrity": "sha512-T1RB3KQdskh9t3v/qv7niK6P8yvn7ja1mS7QK7XfRVL6wtZ8/mFs/FHf4fKvTA0rKnqnYxl/uHFNbnEt0phgbw==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -1955,16 +1955,16 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "8.2.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.2.0.tgz",
-      "integrity": "sha512-kiG4EDUT4dImplOsbh47B1QnNmXSoUqOjWDvCJw/o8LgfD0yr7k2uy54D5Wm0j4t71Ge1NkynGhpWdS0dEIAUA==",
+      "version": "8.4.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.4.0.tgz",
+      "integrity": "sha512-kJ2OIP4dQw5gdI4uXsaxUZHRwWAGpREJ9Zq6D5L0BweyOrWsL6Sz0YcAZGWhvKnH7fm1J5YFE1JrQL0c9dd53A==",
       "dev": true,
       "license": "BSD-2-Clause",
       "dependencies": {
-        "@typescript-eslint/types": "8.2.0",
-        "@typescript-eslint/visitor-keys": "8.2.0",
+        "@typescript-eslint/types": "8.4.0",
+        "@typescript-eslint/visitor-keys": "8.4.0",
         "debug": "^4.3.4",
-        "globby": "^11.1.0",
+        "fast-glob": "^3.3.2",
         "is-glob": "^4.0.3",
         "minimatch": "^9.0.4",
         "semver": "^7.6.0",
@@ -1984,16 +1984,16 @@
       }
     },
     "node_modules/@typescript-eslint/utils": {
-      "version": "8.2.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.2.0.tgz",
-      "integrity": "sha512-O46eaYKDlV3TvAVDNcoDzd5N550ckSe8G4phko++OCSC1dYIb9LTc3HDGYdWqWIAT5qDUKphO6sd9RrpIJJPfg==",
+      "version": "8.4.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.4.0.tgz",
+      "integrity": "sha512-swULW8n1IKLjRAgciCkTCafyTHHfwVQFt8DovmaF69sKbOxTSFMmIZaSHjqO9i/RV0wIblaawhzvtva8Nmm7lQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
-        "@typescript-eslint/scope-manager": "8.2.0",
-        "@typescript-eslint/types": "8.2.0",
-        "@typescript-eslint/typescript-estree": "8.2.0"
+        "@typescript-eslint/scope-manager": "8.4.0",
+        "@typescript-eslint/types": "8.4.0",
+        "@typescript-eslint/typescript-estree": "8.4.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2007,13 +2007,13 @@
       }
     },
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "8.2.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.2.0.tgz",
-      "integrity": "sha512-sbgsPMW9yLvS7IhCi8IpuK1oBmtbWUNP+hBdwl/I9nzqVsszGnNGti5r9dUtF5RLivHUFFIdRvLiTsPhzSyJ3Q==",
+      "version": "8.4.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.4.0.tgz",
+      "integrity": "sha512-zTQD6WLNTre1hj5wp09nBIDiOc2U5r/qmzo7wxPn4ZgAjHql09EofqhF9WF+fZHzL5aCyaIpPcT2hyxl73kr9A==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.2.0",
+        "@typescript-eslint/types": "8.4.0",
         "eslint-visitor-keys": "^3.4.3"
       },
       "engines": {
@@ -2142,9 +2142,9 @@
       }
     },
     "node_modules/apexcharts": {
-      "version": "3.52.0",
-      "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.52.0.tgz",
-      "integrity": "sha512-7dg0ADKs8AA89iYMZMe2sFDG0XK5PfqllKV9N+i3hKHm3vEtdhwz8AlXGm+/b0nJ6jKiaXsqci5LfVxNhtB+dA==",
+      "version": "3.53.0",
+      "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.53.0.tgz",
+      "integrity": "sha512-QESZHZY3w9LPQ64PGh1gEdfjYjJ5Jp+Dfy0D/CLjsLOPTpXzdxwlNMqRj+vPbTcP0nAHgjWv1maDqcEq6u5olw==",
       "license": "MIT",
       "dependencies": {
         "@yr/monotone-cubic-spline": "^1.0.3",
@@ -2215,16 +2215,6 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/array-union": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
-      "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=8"
-      }
-    },
     "node_modules/arraybuffer.prototype.slice": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz",
@@ -2460,9 +2450,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001653",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001653.tgz",
-      "integrity": "sha512-XGWQVB8wFQ2+9NZwZ10GxTYC5hk0Fa+q8cSkr0tgvMhYhMHP/QC+WTgrePMDBWiWc/pV+1ik82Al20XOK25Gcw==",
+      "version": "1.0.30001655",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz",
+      "integrity": "sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg==",
       "dev": true,
       "funding": [
         {
@@ -2859,19 +2849,6 @@
       "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
       "license": "Apache-2.0"
     },
-    "node_modules/dir-glob": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
-      "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "path-type": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
     "node_modules/dlv": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
@@ -3096,9 +3073,9 @@
       }
     },
     "node_modules/escalade": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
-      "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -3589,15 +3566,15 @@
       }
     },
     "node_modules/flowbite-svelte": {
-      "version": "0.46.15",
-      "resolved": "https://registry.npmjs.org/flowbite-svelte/-/flowbite-svelte-0.46.15.tgz",
-      "integrity": "sha512-xWhyLDez/gafTAmQayPMPKmWj1BAoq80SoA48yHZ12Wk3Vu3hFrELLUZf0UksxnjQL9hMOKRUlYl3/IH6pHwnQ==",
+      "version": "0.46.16",
+      "resolved": "https://registry.npmjs.org/flowbite-svelte/-/flowbite-svelte-0.46.16.tgz",
+      "integrity": "sha512-NkyMS/d1EwuL1cqstSUflnG9vhhBiNyUiAw51D8lfPKDfUG1iXc4+HueQw01zhHv3uSXRJRToFBrg6npxeJ3jw==",
       "license": "MIT",
       "dependencies": {
-        "@floating-ui/dom": "^1.6.7",
-        "apexcharts": "^3.49.2",
-        "flowbite": "^2.4.1",
-        "tailwind-merge": "^2.3.0"
+        "@floating-ui/dom": "^1.6.10",
+        "apexcharts": "^3.53.0",
+        "flowbite": "^2.5.1",
+        "tailwind-merge": "^2.5.2"
       },
       "engines": {
         "node": ">=18.0.0",
@@ -3898,27 +3875,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/globby": {
-      "version": "11.1.0",
-      "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
-      "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "array-union": "^2.1.0",
-        "dir-glob": "^3.0.1",
-        "fast-glob": "^3.2.9",
-        "ignore": "^5.2.0",
-        "merge2": "^1.4.1",
-        "slash": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/globrex": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
@@ -4570,9 +4526,9 @@
       }
     },
     "node_modules/jose": {
-      "version": "5.7.0",
-      "resolved": "https://registry.npmjs.org/jose/-/jose-5.7.0.tgz",
-      "integrity": "sha512-3P9qfTYDVnNn642LCAqIKbTGb9a1TBxZ9ti5zEVEr48aDdflgRjhspWFb6WM4PzAfFbGMJYC4+803v8riCRAKw==",
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/jose/-/jose-5.8.0.tgz",
+      "integrity": "sha512-E7CqYpL/t7MMnfGnK/eg416OsFCVUrU/Y3Vwe7QjKhu/BkS1Ms455+2xsqZQVN57/U2MHMBvEb5SrmAZWAIntA==",
       "license": "MIT",
       "funding": {
         "url": "https://github.com/sponsors/panva"
@@ -5067,9 +5023,9 @@
       "license": "MIT"
     },
     "node_modules/nodemailer": {
-      "version": "6.9.14",
-      "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.14.tgz",
-      "integrity": "sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==",
+      "version": "6.9.15",
+      "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.15.tgz",
+      "integrity": "sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==",
       "license": "MIT-0",
       "engines": {
         "node": ">=6.0.0"
@@ -5347,9 +5303,9 @@
       }
     },
     "node_modules/oauth4webapi": {
-      "version": "2.12.0",
-      "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.12.0.tgz",
-      "integrity": "sha512-WFmcHzhFtq2Ar91crpGQZUD8DS0SG7Zti1AgbansUAfdpIsoRXE+hcMNi8MW6bGNNObWis0x8BZRl6K+FR4oQg==",
+      "version": "2.12.1",
+      "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.12.1.tgz",
+      "integrity": "sha512-7lgyz74z5NJ1dRbZSnL/YCJt7UMhNOqQ8I6Jokh/Et66GVK6KfFikzU98i5PTbgbrsSmxzsQprJdI3Z+5Uwj2Q==",
       "license": "MIT",
       "funding": {
         "url": "https://github.com/sponsors/panva"
@@ -5558,13 +5514,16 @@
       }
     },
     "node_modules/path-type": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
-      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
+      "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
       "dev": true,
       "license": "MIT",
+      "dependencies": {
+        "pify": "^3.0.0"
+      },
       "engines": {
-        "node": ">=8"
+        "node": ">=4"
       }
     },
     "node_modules/path2d": {
@@ -5578,9 +5537,9 @@
       }
     },
     "node_modules/pdfjs-dist": {
-      "version": "4.5.136",
-      "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.5.136.tgz",
-      "integrity": "sha512-V1BALcAN/FmxBEShLxoP73PlQZAZtzlaNfRbRhJrKvXzjLC5VaIlBAQUJuWP8iaYUmIdmdLHmt3E2TBglxOm3w==",
+      "version": "4.6.82",
+      "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.6.82.tgz",
+      "integrity": "sha512-BUOryeRFwvbLe0lOU6NhkJNuVQUp06WxlJVVCsxdmJ4y5cU3O3s3/0DunVdK1PMm7v2MUw52qKYaidhDH1Z9+w==",
       "license": "Apache-2.0",
       "engines": {
         "node": ">=18"
@@ -5591,9 +5550,9 @@
       }
     },
     "node_modules/picocolors": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
-      "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
+      "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
       "license": "ISC"
     },
     "node_modules/picomatch": {
@@ -5651,9 +5610,9 @@
       }
     },
     "node_modules/postcss": {
-      "version": "8.4.41",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
-      "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
+      "version": "8.4.45",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz",
+      "integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==",
       "funding": [
         {
           "type": "opencollective",
@@ -5968,19 +5927,6 @@
         "node": ">=4"
       }
     },
-    "node_modules/read-pkg/node_modules/path-type": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
-      "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "pify": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/readable-stream": {
       "version": "3.6.2",
       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -6123,9 +6069,9 @@
       }
     },
     "node_modules/rollup": {
-      "version": "4.21.0",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.0.tgz",
-      "integrity": "sha512-vo+S/lfA2lMS7rZ2Qoubi6I5hwZwzXeUIctILZLbHI+laNtvhhOIon2S1JksA5UEDQ7l3vberd0fxK44lTYjbQ==",
+      "version": "4.21.2",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.2.tgz",
+      "integrity": "sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -6139,22 +6085,22 @@
         "npm": ">=8.0.0"
       },
       "optionalDependencies": {
-        "@rollup/rollup-android-arm-eabi": "4.21.0",
-        "@rollup/rollup-android-arm64": "4.21.0",
-        "@rollup/rollup-darwin-arm64": "4.21.0",
-        "@rollup/rollup-darwin-x64": "4.21.0",
-        "@rollup/rollup-linux-arm-gnueabihf": "4.21.0",
-        "@rollup/rollup-linux-arm-musleabihf": "4.21.0",
-        "@rollup/rollup-linux-arm64-gnu": "4.21.0",
-        "@rollup/rollup-linux-arm64-musl": "4.21.0",
-        "@rollup/rollup-linux-powerpc64le-gnu": "4.21.0",
-        "@rollup/rollup-linux-riscv64-gnu": "4.21.0",
-        "@rollup/rollup-linux-s390x-gnu": "4.21.0",
-        "@rollup/rollup-linux-x64-gnu": "4.21.0",
-        "@rollup/rollup-linux-x64-musl": "4.21.0",
-        "@rollup/rollup-win32-arm64-msvc": "4.21.0",
-        "@rollup/rollup-win32-ia32-msvc": "4.21.0",
-        "@rollup/rollup-win32-x64-msvc": "4.21.0",
+        "@rollup/rollup-android-arm-eabi": "4.21.2",
+        "@rollup/rollup-android-arm64": "4.21.2",
+        "@rollup/rollup-darwin-arm64": "4.21.2",
+        "@rollup/rollup-darwin-x64": "4.21.2",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.21.2",
+        "@rollup/rollup-linux-arm-musleabihf": "4.21.2",
+        "@rollup/rollup-linux-arm64-gnu": "4.21.2",
+        "@rollup/rollup-linux-arm64-musl": "4.21.2",
+        "@rollup/rollup-linux-powerpc64le-gnu": "4.21.2",
+        "@rollup/rollup-linux-riscv64-gnu": "4.21.2",
+        "@rollup/rollup-linux-s390x-gnu": "4.21.2",
+        "@rollup/rollup-linux-x64-gnu": "4.21.2",
+        "@rollup/rollup-linux-x64-musl": "4.21.2",
+        "@rollup/rollup-win32-arm64-msvc": "4.21.2",
+        "@rollup/rollup-win32-ia32-msvc": "4.21.2",
+        "@rollup/rollup-win32-x64-msvc": "4.21.2",
         "fsevents": "~2.3.2"
       }
     },
@@ -6544,16 +6490,6 @@
         "node": ">= 10"
       }
     },
-    "node_modules/slash": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
-      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=8"
-      }
-    },
     "node_modules/sorcery": {
       "version": "0.11.1",
       "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.1.tgz",
@@ -6869,24 +6805,24 @@
       }
     },
     "node_modules/svelte": {
-      "version": "5.0.0-next.238",
-      "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.0.0-next.238.tgz",
-      "integrity": "sha512-fCPNBqQA/MYadkI58LOV3kpYHP5zOsMSjjPagc63Z5LXRkdZe/TKOJbVtLK9Gp4+Shf3qTaxBCp/BR1845Rd/A==",
+      "version": "5.0.0-next.243",
+      "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.0.0-next.243.tgz",
+      "integrity": "sha512-+oXjRInUyBfZXAEY8hmpf3F0eghAVCoWasotz1iOp2G5CyH4KR7jPxWOgjbgsgpL4zlMiN32MEYU1+I+QsC+nQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@ampproject/remapping": "^2.2.1",
-        "@jridgewell/sourcemap-codec": "^1.4.15",
+        "@ampproject/remapping": "^2.3.0",
+        "@jridgewell/sourcemap-codec": "^1.5.0",
         "@types/estree": "^1.0.5",
-        "acorn": "^8.11.3",
+        "acorn": "^8.12.1",
         "acorn-typescript": "^1.4.13",
         "aria-query": "^5.3.0",
-        "axobject-query": "^4.0.0",
+        "axobject-query": "^4.1.0",
         "esm-env": "^1.0.0",
         "esrap": "^1.2.2",
         "is-reference": "^3.0.2",
         "locate-character": "^3.0.0",
-        "magic-string": "^0.30.5",
+        "magic-string": "^0.30.11",
         "zimmerframe": "^1.1.2"
       },
       "engines": {
@@ -7245,9 +7181,9 @@
       }
     },
     "node_modules/tailwindcss/node_modules/yaml": {
-      "version": "2.5.0",
-      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz",
-      "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==",
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
+      "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==",
       "license": "ISC",
       "bin": {
         "yaml": "bin.mjs"
@@ -7594,14 +7530,14 @@
       }
     },
     "node_modules/vite": {
-      "version": "5.4.2",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz",
-      "integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==",
+      "version": "5.4.3",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.3.tgz",
+      "integrity": "sha512-IH+nl64eq9lJjFqU+/yrRnrHPVTlgy42/+IzbOdaFDVlyLgI/wDlf+FCobXLX1cT0X5+7LMyH1mIy2xJdLfo8Q==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "esbuild": "^0.21.3",
-        "postcss": "^8.4.41",
+        "postcss": "^8.4.43",
         "rollup": "^4.20.0"
       },
       "bin": {
@@ -7654,11 +7590,15 @@
       }
     },
     "node_modules/vitefu": {
-      "version": "0.2.5",
-      "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz",
-      "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.2.tgz",
+      "integrity": "sha512-0/iAvbXyM3RiPPJ4lyD4w6Mjgtf4ejTK6TPvTNG3H32PLwuT0N/ZjJLiXug7ETE/LWtTeHw9WRv7uX/tIKYyKg==",
       "dev": true,
       "license": "MIT",
+      "workspaces": [
+        "tests/deps/*",
+        "tests/projects/*"
+      ],
       "peerDependencies": {
         "vite": "^3.0.0 || ^4.0.0 || ^5.0.0"
       },
diff --git a/src/lib/components/MailInput.svelte b/src/lib/components/MailInput.svelte
index 0fd79d3f18380f9c929c60bb14e4a20a253b3436..66992a3739fed0f0c7a5a94cfa2eb926a3424056 100644
--- a/src/lib/components/MailInput.svelte
+++ b/src/lib/components/MailInput.svelte
@@ -83,8 +83,8 @@
 	<Input name="replyTo" bind:value={replyTo} placeholder="Antwortadresse" class="w-full" {disabled} />
 	{:else}
 	<Select name="replyTo" value={replyTo} on:change={e=>{
-		customReplyTo = e.target!.value==="custom";
-		if(!customReplyTo) replyTo = e.target!.value;
+		customReplyTo = (e.target! as HTMLSelectElement).value==="custom";
+		if(!customReplyTo) replyTo = (e.target! as HTMLSelectElement).value;
 		else if(replyTo === "-") replyTo = "";
 	}} {disabled}>
 		<option value={"-"}>keine</option>
@@ -101,7 +101,7 @@
 	{#each locales as locale}
 	<Label class="w-full">
 		Betreff ({locale})
-		<Input name="subject[{locale}]" bind:value={subject[locale]} oninput={e=>subject=Object.assign(subject,{[locale]:e.target.value})} {disabled} required={locale==="de"} />
+		<Input name="subject[{locale}]" bind:value={subject[locale]} on:input={e=>subject=Object.assign(subject,{[locale]:(e.target as HTMLInputElement)!.value})} {disabled} required={locale==="de"} />
 	</Label>
 	{/each}
 </div>
@@ -130,7 +130,7 @@
 		<Span slot="header">Beispiel-Tutor</Span>
 		<Label>
 			Studiengang
-			<Select value={exampleTutor.studyProgram.id} on:change={e=>exampleTutor.studyProgram=studyPrograms.find(s=>s.id==e.target!.value)}>
+			<Select value={exampleTutor.studyProgram.id} on:change={e=>exampleTutor.studyProgram=(studyPrograms.find(s=>s.id==parseInt((e.target! as HTMLSelectElement).value))!)}>
 				{#each studyPrograms as studyProgram}
 				<option value={studyProgram.id}>{studyProgram.name[$locale]}</option>
 				{/each}
@@ -160,7 +160,7 @@
 		</Label>
 		<Label>
 			Schulung
-			<Select value={exampleTutor.training?.id ?? -1} on:change={e=>exampleTutor.training=trainings.find(t=>t.id===e.target!.value)}>
+			<Select value={exampleTutor.training?.id ?? -1} on:change={e=>exampleTutor.training=trainings.find(t=>t.id===(e.target as HTMLSelectElement)!.value)}>
 				<option value={-1}>keine</option>
 				{#each trainings as training}
 				<option value={training.id}>{training.date}</option>
@@ -182,6 +182,7 @@
 	</AccordionItem>
 	<AccordionItem>
 		<Span slot="header">Formatierer</Span>
+		<P class="mb-2">Formatierer können mit Argumenten erstellt werden. Dafür nutzt man <code>formatierer:argument</code>, wobei das Argument gültiges JSON sein muss. Es muss darauf geachtet werden, dass sich kein <code>{"}}"}</code> bildet.</P>
 		<List tag="ul" position="outside" class="ml-4">
 			{#each Object.entries(formatters) as [name, {description}]}
 			<Li><code>{name}</code> - {description}</Li>
@@ -191,7 +192,7 @@
 	<AccordionItem>
 		<Span slot="header">Bedingungen</Span>
 		<List tag="ul" position="outside" class="ml-4">
-			{#each conditions as condition}
+			{#each conditions[type] as condition}
 			<Li>
 				<code>{"{{if "}{condition.name}{#if condition.arguments}{" ...args"}{/if}{"}}"}</code> - {condition.description}
 				{#if condition.arguments}
@@ -216,6 +217,7 @@
 					<b>Formatierung</b>
 					<List tag="ul" position="outside" class="ml-6">
 						<Li>Werte von Variablen können formatiert werden, indem die Formatierer getrennt durch <code>|</code> nach dem Variablennamen gelistet werden, z.B. <code>{"{{"}variablenname|formatierer1|formatierer2{"}}"}</code>. Die Formatierer werden in der angegebenen Reihenfolge angewandt.</Li>
+						<Li>Unterstützt ein Formatierer Konstruktorenargumente, so können diese mit <code>:</code> getrennt hinter dem Namen angegeben werden. Das Argument muss gültiges JSON sein und kein frühzeitiges {"}}"} bilden. Beispiel: <code>{"{{"}argument|date:{"{"}"year":"2-digit"{"} "}{"}}"}</code>, wobei das Leerzeichen am Ende wichtig ist, weil sich sonst {"}}}"} bilden würde.</Li>
 					</List>
 				</Li>
 				<Li>
diff --git a/src/lib/components/SortableTable.svelte b/src/lib/components/SortableTable.svelte
index 074766cb3fb4dbef4afc7f3d4397306ed98ea7fa..ee913483e6f87dbf83eeb682f3c8d74bd27f1975 100644
--- a/src/lib/components/SortableTable.svelte
+++ b/src/lib/components/SortableTable.svelte
@@ -11,28 +11,10 @@
 	} & Record<string, any>;
 	let { items, row, children, ...restProps }: ComponentProps = $props();
 	
-	const sorting = writable({sorted: items, sortDirection: 1, sorter: null});
+	const sorting = writable({sorted: items, sortDirection: 1, sorter: ""});
 	setContext("sorting", sorting);
 </script>
 
-<style>
-	div :global(.sortable) {
-		cursor: pointer;
-		position: relative;
-	}
-	div :global(.sortable::after) {
-		content: "";
-		position: absolute;
-		padding-left: .7rem;
-	}
-	div :global(.sorting-asc::after) {
-		content: "▲";
-	}
-	div :global(.sorting-desc::after) {
-		content: "▼";
-	}
-</style>
-
 <div>
 	<Table {...restProps}>
 		{@render children()}
diff --git a/src/lib/components/SortableTableHeadCell.svelte b/src/lib/components/SortableTableHeadCell.svelte
index 08cffc0f7f03967731f94c0c97891d5ec608b946..98df83f375f12b664bfff09b58cb9f328c14a73e 100644
--- a/src/lib/components/SortableTableHeadCell.svelte
+++ b/src/lib/components/SortableTableHeadCell.svelte
@@ -5,39 +5,30 @@
 	import { twMerge } from "tailwind-merge";
 	
 	type T = $$Generic;
-	const sorting = getContext("sorting") as Writable<{sorted: T[], sortDirection: 1|-1, sorter?: TableHeadCell}>;
+	const sorting = getContext("sorting") as Writable<{sorted: T[], sortDirection: 1|-1, sorter: string}>;
 	
-	let { sort, children, padding="px-6 py-3", defaultDirection="asc", default: def }: { sort: (a: T, b: T)=>number, children: Snippet, padding?: string, defaultDirection?: "asc"|"desc", default?: boolean } = $props();
-	let self: TableHeadCell|undefined = $state();
+	let { sort, children, padding="px-6 py-3", defaultDirection="asc", default: def, buttonClass="" }: { sort: (a: T, b: T)=>number, children: Snippet, padding?: string, defaultDirection?: "asc"|"desc", default?: boolean, buttonClass?: string } = $props();
+	const self = Math.random().toString(36).substring(2);
 	
 	if(def){
-		// update directly for SSR
 		sorting.update(({sorted}) => {
 			let dir = (defaultDirection === "asc" ? 1 : -1) as 1 | -1;
 			return {sorted: sorted.sort((a,b)=>dir*sort(a,b)), sortDirection: dir, sorter: self};
 		});
-		// update on initialization afte "self" has been initialized
-		onMount(()=>{
-			sorting.update(({sorted}) => {
-				let dir = (defaultDirection === "asc" ? 1 : -1) as 1 | -1;
-				return {sorted: sorted.sort((a,b)=>dir*sort(a,b)), sortDirection: dir, sorter: self};
-			});
-		});
 	}
 	
 	function onclick(){
 		sorting.update(({sorted, sortDirection, sorter}) => {
-			if(sorter === self){
-				return {sorted: sorted.sort((a,b)=>-sortDirection*sort(a,b)), sortDirection: -sortDirection as 1 | -1, sorter};
-			}
-			let dir = (defaultDirection === "asc" ? 1 : -1) as 1 | -1;
-			return {sorted: sorted.sort((a,b)=>dir*sort(a,b)), sortDirection: dir, sorter: self};
+			sortDirection = (sorter === self ? -sortDirection : defaultDirection === "asc" ? 1 : -1) as 1 | -1;
+			return {sorted: sorted.sort((a,b)=>sortDirection*sort(a,b)), sortDirection, sorter: self};
 		});
 	}
+	
+	let sortDirection = $derived($sorting.sorter === self ? $sorting.sortDirection === 1 ? "asc" : "desc" : null);
 </script>
 
-<TableHeadCell bind:this={self} padding="">
-	<button {onclick} class={twMerge(padding, "sortable w-full text-left", $sorting.sorter === self && `sorting-${$sorting.sortDirection === 1 ? "asc": "desc"}`)}>
+<TableHeadCell padding="" aria-sort={sortDirection ? `${sortDirection}ending` : undefined}>
+	<button {onclick} class={twMerge(padding, "w-full text-left hover:bg-black hover:bg-opacity-10 relative after:absolute after:pl-3", sortDirection === "asc" && "after:content-['▲']", sortDirection === "desc" && "after:content-['▼']", buttonClass)}>
 		{@render children()}
 	</button>
 </TableHeadCell>
diff --git a/src/lib/mail.ts b/src/lib/mail.ts
index fd0fea3f9070266388cd89b8568793bd9c9592ea..07782fd104ebccb2e197e7938b391d0317f1e0ae 100644
--- a/src/lib/mail.ts
+++ b/src/lib/mail.ts
@@ -5,6 +5,7 @@ import type { Tutor } from "./server/database/entities/Tutor.entity";
 import markdownit from "markdown-it";
 import type { Localized } from "./utils";
 import type { Config } from "./server/database/entities/Config.entity";
+import type { DateTimeFormatOptions } from "intl";
 
 const md = markdownit({
 	breaks: true,
@@ -21,7 +22,11 @@ export type MailTemplate = {
 	type: TemplateType;
 };
 
-export const formatters: Record<string, { description: string, format: (l: Locale, arg: unknown)=>string }> = {
+export const formatters: Record<string, { description: string, format: (l: Locale, arg: unknown, constructorArg: unknown|null)=>string }> = {
+	date: {
+		description: "Formatiert ein Datum, Kontruktorargumente sind die Optionen für Intl.DateTimeFormat (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options)",
+		format: (locale, arg, constructorArg)=>new Intl.DateTimeFormat(locale, constructorArg as DateTimeFormatOptions ?? undefined).format(arg as Date),
+	},
 	dateLong: {
 		description: "Formatiert ein Datum, z.B. 01. Januar 1970",
 		format: (locale, arg)=>new Intl.DateTimeFormat(locale, { year: "numeric", month: "long", day: "2-digit" }).format(arg as Date),
@@ -38,6 +43,10 @@ export const formatters: Record<string, { description: string, format: (l: Local
 		description: "Gibt den Wochentag eines Datums zurück, z.B. Mo",
 		format: (locale, arg)=>new Intl.DateTimeFormat(locale, { weekday: "short" }).format(arg as Date),
 	},
+	dateRange: {
+		description: "Formatiert einen Datumsbereich, Konsruktorargumente sind die Optionen für Intl.DateTimeFormat (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options)",
+		format: (locale, arg, constructorArg)=>new Intl.DateTimeFormat(locale, constructorArg as DateTimeFormatOptions ?? undefined).formatRange((arg as [Date, Date])[0], (arg as [Date, Date])[1]),
+	},
 	dateRangeLong: {
 		description: "Formatiert einen Datumsbereich, z.B. 1. - 2. Januar 1970",
 		format: (locale, arg)=>new Intl.DateTimeFormat(locale, { year: "numeric", month: "long", day: "2-digit" }).formatRange((arg as [Date, Date])[0], (arg as [Date, Date])[1]),
@@ -375,10 +384,15 @@ export function parseTemplate(type: TemplateType, template: Localized|string){
 				const [variableName, ...formatterNames] = name.split("|");
 				const variable = variables[type].find(v=>v.name === variableName);
 				if(!variable) throw new Error(`Unknown variable ${name}`);
-				const fs = formatterNames.map(f=>formatters[f]);
-				if(fs.some(f=>!f)) throw new Error(`Unknown formatter ${formatterNames.find((_, i)=>!fs[i])}`);
+				const fs = formatterNames.map(f=>{
+					const [formatterName, ...formatterArgs] = f.split(":");
+					const formatter = formatters[formatterName];
+					if(!formatter) throw new Error(`Unknown formatter ${formatterName}`);
+					const arg = formatterArgs.join(":");
+					return { format: formatter.format, arg: arg ? JSON.parse(arg) : null };
+				});
 				const replacement = (t: Tutor, l: Locale, c: PartialConfig)=>{
-					return fs.reduce((v, f)=>f.format(l, v), variable.replacement(t, l, c));
+					return fs.reduce((v, f)=>f.format(l, v, f.arg), variable.replacement(t, l, c));
 				};
 				parts.push({type: "variable", replacement});
 			}
@@ -425,10 +439,15 @@ function consumeCondition(type: TemplateType, tokens: Token[]): [Part[], Part[]]
 				const [variableName, ...formatterNames] = name.split("|");
 				const variable = variables[type].find(v=>v.name === variableName);
 				if(!variable) throw new Error(`Unknown variable ${name}`);
-				const fs = formatterNames.map(f=>formatters[f]);
-				if(fs.some(f=>!f)) throw new Error(`Unknown formatter ${formatterNames.find((_, i)=>!fs[i])}`);
+				const fs = formatterNames.map(f=>{
+					const [formatterName, ...formatterArgs] = f.split(":");
+					const formatter = formatters[formatterName];
+					if(!formatter) throw new Error(`Unknown formatter ${formatterName}`);
+					const arg = formatterArgs.join(":");
+					return { format: formatter.format, arg: arg ? JSON.parse(arg) : null };
+				});
 				const replacement = (t: Tutor, l: Locale, c: PartialConfig)=>{
-					return fs.reduce((v, f)=>f.format(l, v), variable.replacement(t, l, c));
+					return fs.reduce((v, f)=>f.format(l, v, f.arg), variable.replacement(t, l, c));
 				};
 				if(inElse) elseParts.push({type: "variable", replacement});
 				else thenParts.push({type: "variable", replacement});
diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts
index 0c5c679074282f7098a8d6872e0f9b39be6467c0..c36770bd3a68ec544624e865619a31cb509aa1d4 100644
--- a/src/lib/server/auth.ts
+++ b/src/lib/server/auth.ts
@@ -96,13 +96,13 @@ export const handleAuthorization: Handle = async ({event, resolve})=>{
 		event.locals.user = user;
 	}else if(event.url.pathname.startsWith("/intern/tutor/") || event.url.pathname === "/intern/tutor"){
 		const session = await event.locals.auth();
-		if(!session || session.user.type !== "tutor") redirect(303, "/intern#tutor");
+		if(!session || session.user.type !== "tutor") redirect(303, `/intern?redirect_url=${encodeURIComponent(event.url.href)}#tutor`);
 		const tutor = await Tutor.getById(session.user.userId);
 		if(!tutor) redirect(303, "/intern");
 		event.locals.tutor = tutor;
 	}else if(event.url.pathname.startsWith("/intern/rallye/") || event.url.pathname === "/intern/rallye"){
 		const session = await event.locals.auth();
-		if(!session || session.user.type !== "rally") redirect(303, "/intern#rallye");
+		if(!session || session.user.type !== "rally") redirect(303, `/intern?redirect_url=${encodeURIComponent(event.url.href)}#rallye`);
 		const supervisor = await RallyStationSupervisor.getById(session.user.userId);
 		if(!supervisor) redirect(303, "/intern");
 		event.locals.supervisor = supervisor;
diff --git a/src/lib/server/database/migrations/0_init.sql b/src/lib/server/database/migrations/0_init.sql
index fee540882a3c0b91633f4a329f72161df3165e08..819bc3eb717fff48fec492ef504ce4a7f2c92746 100644
--- a/src/lib/server/database/migrations/0_init.sql
+++ b/src/lib/server/database/migrations/0_init.sql
@@ -177,3 +177,20 @@ CREATE TABLE  IF NOT EXISTS master_freshers (
 	"study_program" TEXT NOT NULL,
 	"aachen_experience" TEXT NOT NULL
 );
+
+CREATE TABLE IF NOT EXISTS mrx (
+	"id" SERIAL PRIMARY KEY
+);
+
+CREATE TABLE IF NOT EXISTS mrx_entries (
+	"id" SERIAL PRIMARY KEY,
+	"mrx" INT NOT NULL REFERENCES mrx(id),
+	"date" DATE NOT NULL,
+	"text" TEXT NOT NULL,
+);
+
+CREATE TABLE IF NOT EXISTS mrx_attachments (
+	"id" SERIAL PRIMARY KEY,
+	"mrx_entry" INT NOT NULL REFERENCES mrx_entries(id),
+	"path" TEXT NOT NULL,
+);
diff --git a/src/routes/(non-admin)/mr-x/+page.server.ts b/src/routes/(non-admin)/mr-x/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/routes/(non-admin)/mr-x/+page.svelte b/src/routes/(non-admin)/mr-x/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/routes/admin/rabatte/[id=number]/+page.svelte b/src/routes/admin/rabatte/[id=number]/+page.svelte
index ecf70bcbf8456e7371a0a489fdbb19e8b5908209..0c8255e44a4aff815b425c54a227936fe34ccf54 100644
--- a/src/routes/admin/rabatte/[id=number]/+page.svelte
+++ b/src/routes/admin/rabatte/[id=number]/+page.svelte
@@ -51,8 +51,6 @@
 	function roundTo5Digits(num: number): number{
 		return Math.round(num * 1e5) / 1e5;
 	}
-	function refocusMap(address: string): void;
-	function refocusMap(latitude: number, longitude: number): void;
 	async function refocusMap(){
 		if(arguments.length === 1){
 			const [address] = arguments;
diff --git a/src/routes/admin/tutor/+page.svelte b/src/routes/admin/tutor/+page.svelte
index 18206e1e69a287886ce4e9ac020b03fa3f215f2c..09d9dd43b5ac74c0a70a085177e61ca01862ff24 100644
--- a/src/routes/admin/tutor/+page.svelte
+++ b/src/routes/admin/tutor/+page.svelte
@@ -5,66 +5,35 @@
 	import { Permission } from "$lib/perms.js";
 	import { toCSV } from "$lib/csv";
 	import type { Tutor } from "$lib/server/database/entities/Tutor.entity";
+	import SortableTable from "$lib/components/SortableTable.svelte";
+	import SortableTableHeadCell from "$lib/components/SortableTableHeadCell.svelte";
 	
 	export let data;
 	
-	let sortedTutors = data.tutors;
-	
 	$: dateFormatter = new Intl.DateTimeFormat($locale, { day: "2-digit", month: "2-digit", year: "numeric" });
-	type SortProp = string|number|null|undefined;
-	type SortKey = ObjectToDotProp<typeof data.tutors[number], SortProp>;
-	let sortKey: SortKey = "firstname";
-	let sortDirection: -1|1 = 1;
-	const sortTable = (key: SortKey) => {
-		if (sortKey === key) {
-			sortDirection *= -1;
-		} else {
-			sortKey = key;
-			sortDirection = 1;
-		}
-	}
-	$: {
-		sortedTutors = [...sortedTutors].sort((a, b) => {
-			const aVal = sortKey!.split(".").reduce((val, key) => val?.[key], a) as unknown as SortProp;
-			const bVal = sortKey!.split(".").reduce((val, key) => val?.[key], b) as unknown as SortProp;
-			if(aVal == null && bVal == null) return 0;
-			else if(aVal == null) return sortDirection;
-			else if(bVal == null) return -sortDirection;
-			else if(aVal < bVal) return -sortDirection;
-			else if(aVal > bVal) return sortDirection;
-			else return 0;
-		});
-	}
-	
-	function getClassName(key: SortKey, sortKey: SortKey, sortDirection: -1|1){
-		if(sortKey === key){
-			return sortDirection === 1 ? "thc sorting-asc" : "thc sorting-desc";
-		}
-		return "thc";
-	}
 	
 	let showExportModal = false;
 	let exportType = "json";
-	const fields: { prop: ObjectToDotProp<Tutor>, label: string, value: string }[] = [
-		{ prop: "firstname", label: "Vorname", value: "firstname" },
-		{ prop: "lastname", label: "Nachname", value: "lastname" },
-		{ prop: "nickname", label: "Spitzname", value: "nickname" },
-		{ prop: "studyProgram.name.de", label: "Fach", value: "studyProgram" },
-		{ prop: "training.date", label: "Schulung", value: "training" },
-		{ prop: "notes", label: "Notiz", value: "notes" },
+	const fields: { prop: (t: Tutor)=>unknown, label: string, value: string }[] = [
+		{ prop: t=>t.firstname, label: "Vorname", value: "firstname" },
+		{ prop: t=>t.lastname, label: "Nachname", value: "lastname" },
+		{ prop: t=>t.nickname, label: "Spitzname", value: "nickname" },
+		{ prop: t=>t.studyProgram.name.de, label: "Fach", value: "studyProgram" },
+		{ prop: t=>t.training?.date, label: "Schulung", value: "training" },
+		{ prop: t=>t.notes, label: "Notiz", value: "notes" },
 		...(data.user.permissions.has(Permission.VIEW_TUTOR_DETAILS) ? [
-			{ prop: "gender", label: "Geschlecht", value: "gender" },
-			{ prop: "mentor", label: "Mentor", value: "mentor" },
-			{ prop: "shirtSize", label: "Shirt", value: "shirtSize" },
-			{ prop: "coTutorWish", label: "Co-Tutor", value: "coTutorWish" },
-			{ prop: "trained", label: "Geschult", value: "trained" },
-			{ prop: "degree", label: "Abschluss", value: "degree" },
-			{ prop: "email", label: "E-Mail", value: "email" },
-			{ prop: "phone", label: "Telefon", value: "phone" },
-			{ prop: "address", label: "Adresse", value: "address" },
-			{ prop: "birthday", label: "Geburtstag", value: "birthday" },
-			{ prop: "dietaryRestriction", label: "Essgewohnheiten", value: "dietaryRestriction" },
-		] satisfies {prop: ObjectToDotProp<Tutor>, label: string, value: string}[] : []),
+			{ prop: t=>t.gender, label: "Geschlecht", value: "gender" },
+			{ prop: t=>t.mentor, label: "Mentor", value: "mentor" },
+			{ prop: t=>t.shirtSize, label: "Shirt", value: "shirtSize" },
+			{ prop: t=>t.coTutorWish, label: "Co-Tutor", value: "coTutorWish" },
+			{ prop: t=>t.trained, label: "Geschult", value: "trained" },
+			{ prop: t=>t.degree, label: "Abschluss", value: "degree" },
+			{ prop: t=>t.email, label: "E-Mail", value: "email" },
+			{ prop: t=>t.phone, label: "Telefon", value: "phone" },
+			{ prop: t=>t.address, label: "Adresse", value: "address" },
+			{ prop: t=>t.birthday, label: "Geburtstag", value: "birthday" },
+			{ prop: t=>t.dietaryRestriction, label: "Essgewohnheiten", value: "dietaryRestriction" },
+		] satisfies { prop: (t: Tutor)=>unknown, label: string, value: string }[] : []),
 	];
 	let selectedFields: string[] = fields.map(f => f.value);
 	function exportTutors(){
@@ -72,7 +41,7 @@
 			const tutor: Record<string, unknown> = {};
 			for(const field of fields){
 				if(selectedFields.includes(field.value)){
-					tutor[field.value] = field.prop.split(".").reduce((val, key) => val?.[key], t);
+					tutor[field.value] = field.prop(t);
 				}
 			}
 			return tutor;
@@ -96,25 +65,6 @@
 	}
 </script>
 
-<style>
-	div :global(.thc) {
-		cursor: pointer;
-		position: relative;
-	}
-	div :global(.thc::after) {
-		content: "";
-		position: absolute;
-		right: 0;
-		padding-right: 1rem;
-	}
-	div :global(.sorting-asc::after) {
-		content: " ▲";
-	}
-	div :global(.sorting-desc::after) {
-		content: " ▼";
-	}
-</style>
-
 <Breadcrumb class="mb-4">
 	<BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem>
 	<BreadcrumbItem href="/admin/tutor">Tutoren</BreadcrumbItem>
@@ -200,34 +150,39 @@
 </Table>
 
 {#if data.user.permissions.has(Permission.VIEW_TUTORS)}
-<Table class="mb-6" hoverable>
+<SortableTable class="mb-6" hoverable items={data.tutors}>
 	<TableHead>
-		<TableHeadCell on:click={()=>sortTable("firstname")} class={getClassName("firstname", sortKey, sortDirection)}>Vorname</TableHeadCell>
-		<TableHeadCell on:click={()=>sortTable("lastname")} class={getClassName("lastname", sortKey, sortDirection)}>Nachname</TableHeadCell>
-		<TableHeadCell on:click={()=>sortTable(`studyProgram.name.${$locale}`)} class={getClassName(`studyProgram.name.${$locale}`, sortKey, sortDirection)}>Fach</TableHeadCell>
-		<TableHeadCell on:click={()=>sortTable("shirtSize")} class={getClassName("shirtSize", sortKey, sortDirection)}>Shirt</TableHeadCell>
-		<TableHeadCell on:click={()=>sortTable("coTutorWish")} class={getClassName("coTutorWish", sortKey, sortDirection)}>Co-Tutor</TableHeadCell>
-		<TableHeadCell on:click={()=>sortTable("training.date")} class={getClassName("training.date", sortKey, sortDirection)}>Schulung</TableHeadCell>
-		<TableHeadCell on:click={()=>sortTable("notes")} class={getClassName("notes", sortKey, sortDirection)}>Notiz</TableHeadCell>
+		<SortableTableHeadCell sort={(a: Tutor, b)=>a.firstname.localeCompare(b.firstname)}>Vorname</SortableTableHeadCell>
+		<SortableTableHeadCell sort={(a: Tutor, b)=>a.lastname.localeCompare(b.lastname)}>Nachname</SortableTableHeadCell>
+		<SortableTableHeadCell sort={(a: Tutor, b)=>a.studyProgram.name[$locale].localeCompare(b.studyProgram.name[$locale])}>Fach</SortableTableHeadCell>
+		<SortableTableHeadCell sort={(a: Tutor, b)=>a.shirtSize.localeCompare(b.shirtSize)}>T-Shirt</SortableTableHeadCell>
+		<SortableTableHeadCell sort={(a: Tutor, b)=>a.coTutorWish.localeCompare(b.coTutorWish)}>Co-Tutor</SortableTableHeadCell>
+		<SortableTableHeadCell sort={(a: Tutor, b)=>{
+			if(a.training && b.training) return a.training.date.localeCompare(b.training.date);
+			else if(a.training) return -1;
+			else if(b.training) return 1;
+			else return 0;
+		}}>Schulung</SortableTableHeadCell>
+		<SortableTableHeadCell sort={(a: Tutor, b)=>a.notes.localeCompare(b.notes)}>Notiz</SortableTableHeadCell>
+		{#if data.user.permissions.has(Permission.UPDATE_TUTOR)}
 		<TableHeadCell></TableHeadCell>
+		{/if}
 	</TableHead>
-	<TableBody>
-		{#each sortedTutors as tutor}
-		<TableBodyRow>
-			<TableBodyCell>{tutor.firstname}{tutor.nickname?` (${tutor.nickname})`:""}</TableBodyCell>
-			<TableBodyCell>{tutor.lastname}</TableBodyCell>
-			<TableBodyCell>{tutor.studyProgram.name[$locale]}</TableBodyCell>
-			<TableBodyCell>{tutor.shirtSize}</TableBodyCell>
-			<TableBodyCell>{tutor.coTutorWish || ""}</TableBodyCell>
-			<TableBodyCell>{tutor.training?dateFormatter.format(new Date(tutor.training.date)):"Bereits geschult"}</TableBodyCell>
-			<TableBodyCell>{tutor.notes}</TableBodyCell>
-			<TableBodyCell>
-				<Button href={`/admin/tutor/${tutor.id}`} color="light" size="xs">Bearbeiten</Button>
-			</TableBodyCell>
-		</TableBodyRow>
-		{/each}
-	</TableBody>
-</Table>
+	{#snippet row({item: tutor})}
+	<TableBodyRow>
+		<TableBodyCell>{tutor.firstname}{tutor.nickname?` (${tutor.nickname})`:""}</TableBodyCell>
+		<TableBodyCell>{tutor.lastname}</TableBodyCell>
+		<TableBodyCell>{tutor.studyProgram.name[$locale]}</TableBodyCell>
+		<TableBodyCell>{tutor.shirtSize}</TableBodyCell>
+		<TableBodyCell>{tutor.coTutorWish || ""}</TableBodyCell>
+		<TableBodyCell>{tutor.training?dateFormatter.format(new Date(tutor.training.date)):"Bereits geschult"}</TableBodyCell>
+		<TableBodyCell>{tutor.notes}</TableBodyCell>
+		{#if data.user.permissions.has(Permission.UPDATE_TUTOR)}
+		<TableBodyCell>
+			<Button href={`/admin/tutor/${tutor.id}`} color="light" size="xs">Bearbeiten</Button>
+		</TableBodyCell>
+		{/if}
+	</TableBodyRow>
+	{/snippet}
+</SortableTable>
 {/if}
-
-<!-- TODO: auto match co-tutors based on wishes ("npm i modern-diacritics" for names) -->
diff --git a/src/routes/intern/+page.svelte b/src/routes/intern/+page.svelte
index d02a7f6354bca7f472feb1487eb958aa5a25b6a1..58a1bcc769771ca78faf3e5f03b825c0e53964bf 100644
--- a/src/routes/intern/+page.svelte
+++ b/src/routes/intern/+page.svelte
@@ -65,7 +65,7 @@ A merge request was created to get this issue fixed: https://github.com/nextauth
 				}
 			}}>
 				<input type="hidden" name="providerId" value="tutor" />
-				<input type="hidden" name="redirectTo" value="/intern/tutor" />
+				<input type="hidden" name="redirectTo" value={$page.url.searchParams.get("redirect_url") ?? "/intern/tutor"} />
 				<Input type="email" name="email" placeholder="E-Mail" required class="mb-2" />
 				<Input type="password" name="password" placeholder="Passwort" required class="mb-2" />
 				<Button type="submit">Anmelden</Button>
@@ -103,7 +103,7 @@ A merge request was created to get this issue fixed: https://github.com/nextauth
 				}
 			}}>
 				<input type="hidden" name="providerId" value="rally" />
-				<input type="hidden" name="redirectTo" value="/intern/rallye" />
+				<input type="hidden" name="redirectTo" value={$page.url.searchParams.get("redirect_url") ?? "/intern/rallye"} />
 				<Input type="email" name="email" placeholder="E-Mail" required class="mb-2" />
 				<Input type="password" name="password" placeholder="Passwort" required class="mb-2" />
 				<Button type="submit">Anmelden</Button>
diff --git a/src/routes/intern/tutor/+page.svelte b/src/routes/intern/tutor/+page.svelte
index eea990dd895b57f1c0d3094f23cc9a97de8399fe..36cc0537817d4f6a69da370c1dc0d99ef3d9f4dd 100644
--- a/src/routes/intern/tutor/+page.svelte
+++ b/src/routes/intern/tutor/+page.svelte
@@ -1,7 +1,7 @@
 <script lang="ts">
     import SortableTable from "$lib/components/SortableTable.svelte";
     import SortableTableHeadCell from "$lib/components/SortableTableHeadCell.svelte";
-    import { Button, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte";
+    import { Button, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte";
 
 	let { data } = $props();
 	
@@ -19,8 +19,8 @@
 
 <SortableTable {items}>
 	<TableHead>
-		<SortableTableHeadCell sort={(a,b)=>a.name.localeCompare(b.name)}>Name</SortableTableHeadCell>
-		<SortableTableHeadCell sort={(a,b)=>a.age-b.age} defaultDirection="desc" default>Age</SortableTableHeadCell>
+		<SortableTableHeadCell sort={(a: typeof items[number], b)=>a.name.localeCompare(b.name)}>Name</SortableTableHeadCell>
+		<SortableTableHeadCell sort={(a: typeof items[number], b)=>a.age-b.age} defaultDirection="desc">Age</SortableTableHeadCell>
 		<TableHeadCell>Actions</TableHeadCell>
 	</TableHead>
 	{#snippet row({item})}