diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000000000000000000000000000000000000..996cd5aad4d7331e9c74dcaa7eb6483a5116eed9
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,216 @@
+module.exports = {
+  extends: [
+    "eslint:recommended",
+    "plugin:vue/strongly-recommended",
+    //    "plugin:prettier/recommended",
+    "plugin:@intlify/vue-i18n/recommended",
+  ],
+  rules: {
+    "no-unused-vars": "warn",
+    "vue/no-unused-vars": "off",
+    "vue/multi-word-component-names": "off",
+    "@intlify/vue-i18n/key-format-style": [
+      "error",
+      "snake_case",
+      {
+        splitByDots: false,
+      },
+    ],
+    // "@intlify/vue-i18n/no-unused-keys": ["warn", {}],
+    "@intlify/vue-i18n/no-raw-text": [
+      "error",
+      {
+        ignoreNodes: ["v-icon"],
+        ignorePattern: "^[-–—·#:()\\[\\]&\\.\\s]+$",
+      },
+    ],
+    // Fixes for prettier (avoid eslint-config-prettier)
+    // The following rules can be used in some cases. See the README for more
+    // information. (These are marked with `0` instead of `"off"` so that a
+    // script can distinguish them.)
+    curly: 0,
+    "lines-around-comment": 0,
+    "max-len": 0,
+    "no-confusing-arrow": 0,
+    "no-mixed-operators": 0,
+    "no-tabs": 0,
+    "no-unexpected-multiline": 0,
+    quotes: 0,
+    "@typescript-eslint/quotes": 0,
+    "babel/quotes": 0,
+    "vue/html-self-closing": 0,
+    "vue/max-len": 0,
+
+    // The rest are rules that you never need to enable when using Prettier.
+    "array-bracket-newline": "off",
+    "array-bracket-spacing": "off",
+    "array-element-newline": "off",
+    "arrow-parens": "off",
+    "arrow-spacing": "off",
+    "block-spacing": "off",
+    "brace-style": "off",
+    "comma-dangle": "off",
+    "comma-spacing": "off",
+    "comma-style": "off",
+    "computed-property-spacing": "off",
+    "dot-location": "off",
+    "eol-last": "off",
+    "func-call-spacing": "off",
+    "function-call-argument-newline": "off",
+    "function-paren-newline": "off",
+    "generator-star": "off",
+    "generator-star-spacing": "off",
+    "implicit-arrow-linebreak": "off",
+    indent: "off",
+    "jsx-quotes": "off",
+    "key-spacing": "off",
+    "keyword-spacing": "off",
+    "linebreak-style": "off",
+    "multiline-ternary": "off",
+    "newline-per-chained-call": "off",
+    "new-parens": "off",
+    "no-arrow-condition": "off",
+    "no-comma-dangle": "off",
+    "no-extra-parens": "off",
+    "no-extra-semi": "off",
+    "no-floating-decimal": "off",
+    "no-mixed-spaces-and-tabs": "off",
+    "no-multi-spaces": "off",
+    "no-multiple-empty-lines": "off",
+    "no-reserved-keys": "off",
+    "no-space-before-semi": "off",
+    "no-trailing-spaces": "off",
+    "no-whitespace-before-property": "off",
+    "no-wrap-func": "off",
+    "nonblock-statement-body-position": "off",
+    "object-curly-newline": "off",
+    "object-curly-spacing": "off",
+    "object-property-newline": "off",
+    "one-var-declaration-per-line": "off",
+    "operator-linebreak": "off",
+    "padded-blocks": "off",
+    "quote-props": "off",
+    "rest-spread-spacing": "off",
+    semi: "off",
+    "semi-spacing": "off",
+    "semi-style": "off",
+    "space-after-function-name": "off",
+    "space-after-keywords": "off",
+    "space-before-blocks": "off",
+    "space-before-function-paren": "off",
+    "space-before-function-parentheses": "off",
+    "space-before-keywords": "off",
+    "space-in-brackets": "off",
+    "space-in-parens": "off",
+    "space-infix-ops": "off",
+    "space-return-throw-case": "off",
+    "space-unary-ops": "off",
+    "space-unary-word-ops": "off",
+    "switch-colon-spacing": "off",
+    "template-curly-spacing": "off",
+    "template-tag-spacing": "off",
+    "unicode-bom": "off",
+    "wrap-iife": "off",
+    "wrap-regex": "off",
+    "yield-star-spacing": "off",
+    "@babel/object-curly-spacing": "off",
+    "@babel/semi": "off",
+    "@typescript-eslint/brace-style": "off",
+    "@typescript-eslint/comma-dangle": "off",
+    "@typescript-eslint/comma-spacing": "off",
+    "@typescript-eslint/func-call-spacing": "off",
+    "@typescript-eslint/indent": "off",
+    "@typescript-eslint/keyword-spacing": "off",
+    "@typescript-eslint/member-delimiter-style": "off",
+    "@typescript-eslint/no-extra-parens": "off",
+    "@typescript-eslint/no-extra-semi": "off",
+    "@typescript-eslint/object-curly-spacing": "off",
+    "@typescript-eslint/semi": "off",
+    "@typescript-eslint/space-before-blocks": "off",
+    "@typescript-eslint/space-before-function-paren": "off",
+    "@typescript-eslint/space-infix-ops": "off",
+    "@typescript-eslint/type-annotation-spacing": "off",
+    "babel/object-curly-spacing": "off",
+    "babel/semi": "off",
+    "flowtype/boolean-style": "off",
+    "flowtype/delimiter-dangle": "off",
+    "flowtype/generic-spacing": "off",
+    "flowtype/object-type-curly-spacing": "off",
+    "flowtype/object-type-delimiter": "off",
+    "flowtype/quotes": "off",
+    "flowtype/semi": "off",
+    "flowtype/space-after-type-colon": "off",
+    "flowtype/space-before-generic-bracket": "off",
+    "flowtype/space-before-type-colon": "off",
+    "flowtype/union-intersection-spacing": "off",
+    "react/jsx-child-element-spacing": "off",
+    "react/jsx-closing-bracket-location": "off",
+    "react/jsx-closing-tag-location": "off",
+    "react/jsx-curly-newline": "off",
+    "react/jsx-curly-spacing": "off",
+    "react/jsx-equals-spacing": "off",
+    "react/jsx-first-prop-new-line": "off",
+    "react/jsx-indent": "off",
+    "react/jsx-indent-props": "off",
+    "react/jsx-max-props-per-line": "off",
+    "react/jsx-newline": "off",
+    "react/jsx-one-expression-per-line": "off",
+    "react/jsx-props-no-multi-spaces": "off",
+    "react/jsx-tag-spacing": "off",
+    "react/jsx-wrap-multilines": "off",
+    "standard/array-bracket-even-spacing": "off",
+    "standard/computed-property-even-spacing": "off",
+    "standard/object-curly-even-spacing": "off",
+    "unicorn/empty-brace-spaces": "off",
+    "unicorn/no-nested-ternary": "off",
+    "unicorn/number-literal-case": "off",
+    "vue/array-bracket-newline": "off",
+    "vue/array-bracket-spacing": "off",
+    "vue/arrow-spacing": "off",
+    "vue/block-spacing": "off",
+    "vue/block-tag-newline": "off",
+    "vue/brace-style": "off",
+    "vue/comma-dangle": "off",
+    "vue/comma-spacing": "off",
+    "vue/comma-style": "off",
+    "vue/dot-location": "off",
+    "vue/func-call-spacing": "off",
+    "vue/html-closing-bracket-newline": "off",
+    "vue/html-closing-bracket-spacing": "off",
+    "vue/html-end-tags": "off",
+    "vue/html-indent": "off",
+    "vue/html-quotes": "off",
+    "vue/key-spacing": "off",
+    "vue/keyword-spacing": "off",
+    "vue/max-attributes-per-line": "off",
+    "vue/multiline-html-element-content-newline": "off",
+    "vue/multiline-ternary": "off",
+    "vue/mustache-interpolation-spacing": "off",
+    "vue/no-extra-parens": "off",
+    "vue/no-multi-spaces": "off",
+    "vue/no-spaces-around-equal-signs-in-attribute": "off",
+    "vue/object-curly-newline": "off",
+    "vue/object-curly-spacing": "off",
+    "vue/object-property-newline": "off",
+    "vue/operator-linebreak": "off",
+    "vue/quote-props": "off",
+    "vue/script-indent": "off",
+    "vue/singleline-html-element-content-newline": "off",
+    "vue/space-in-parens": "off",
+    "vue/space-infix-ops": "off",
+    "vue/space-unary-ops": "off",
+    "vue/template-curly-spacing": "off",
+  },
+  settings: {
+    "vue-i18n": {
+      localeDir: "./aleksis/core/frontend/messages/*.{json}",
+      messageSyntaxVersion: "^8.0.0",
+    },
+  },
+  env: {
+    es2021: true,
+  },
+  parserOptions: {
+    ecmaVersion: "latest",
+  },
+};
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f8bcd06c637e4de890c6a8b96312a05f102ca0f8..b2274c0f1da0fa3578ec8730511431f71bb085a9 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,17 +1,17 @@
 include:
-    - project: "AlekSIS/official/AlekSIS"
-      file: /ci/general.yml
-    - project: "AlekSIS/official/AlekSIS"
-      file: /ci/prepare/lock.yml
-    - project: "AlekSIS/official/AlekSIS"
-      file: /ci/test/test.yml
-    - project: "AlekSIS/official/AlekSIS"
-      file: /ci/test/lint.yml
-    - project: "AlekSIS/official/AlekSIS"
-      file: /ci/test/security.yml
-    - project: "AlekSIS/official/AlekSIS"
-      file: /ci/build/dist.yml
-    - project: "AlekSIS/official/AlekSIS"
-      file: /ci/publish/pypi.yml
-    - project: "AlekSIS/official/AlekSIS"
-      file: /ci/docker/image.yml
+  - project: "AlekSIS/official/AlekSIS"
+    file: /ci/general.yml
+  - project: "AlekSIS/official/AlekSIS"
+    file: /ci/prepare/lock.yml
+  - project: "AlekSIS/official/AlekSIS"
+    file: /ci/test/test.yml
+  - project: "AlekSIS/official/AlekSIS"
+    file: /ci/test/lint.yml
+  - project: "AlekSIS/official/AlekSIS"
+    file: /ci/test/security.yml
+  - project: "AlekSIS/official/AlekSIS"
+    file: /ci/build/dist.yml
+  - project: "AlekSIS/official/AlekSIS"
+    file: /ci/publish/pypi.yml
+  - project: "AlekSIS/official/AlekSIS"
+    file: /ci/docker/image.yml
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000000000000000000000000000000000000..38d141b743fd55678f50077c0617924475817095
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,91 @@
+# Byte-compiled / optimized / DLL files
+*$py.class
+*.py[cod]
+__pycache__/
+
+# Distribution / packaging
+*.egg
+*.egg-info/
+.Python
+.eggs/
+.installed.cfg
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+
+# Installer logs
+pip-delete-this-directory.txt
+pip-log.txt
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+
+# pyenv
+.python-version
+
+# Environments
+.env
+.venv
+ENV/
+env/
+venv/
+
+# Editors
+*~
+DEADJOE
+\#*#
+
+# IntelliJ
+.idea
+.idea/
+
+# Database
+db.sqlite3
+
+# Sphinx
+docs/_build/
+
+# TeX
+*.aux
+
+# Generated files
+/node_modules/
+/static/
+/whoosh_index/
+poetry.lock
+
+.coverage
+.mypy_cache/
+.tox/
+htmlcov/
+maintenance_mode_state.txt
+media/
+package-lock.json
+yarn.lock
+
+# VSCode
+.vscode/
+.history/
+*.code-workspace
+
+/cache
+
+# Add HTML files to avoid problems with unsupported Django templates
+*.html
+
+# Do not check/reformat generated files
+aleksis/core/util/licenses.json
+.vite/
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 483c7fb62c21be3feb9f333a0b559d4d4fffbc65..4ecfde832b23b18a66000fc4568e1b896e7e7a6c 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -6,21 +6,36 @@ All notable changes to this project will be documented in this file.
 The format is based on `Keep a Changelog`_,
 and this project adheres to `Semantic Versioning`_.
 
-Breaking changes
-----------------
+Unreleased
+----------
+
+`3.0`_ - 2023-05-15
+-------------------
+
+Fixed
+~~~~~
+* In some cases, pages showing the count of extra marks and lessons with custom excuse types of
+  persons threw an error.
+* The redirection to generated class register PDF printouts did not work.
+* Some columns in the table showing statistics for the members of a group were labled wrongly.
+* Absences with custom excuse types were not counted correctly.
+* Tabs on the week overview page were not displayed.
+
+`3.0b0`_ - 2023-02-28
+---------------------
+
+This version requires AlekSIS-Core 3.0. It is incompatible with any previous
+version.
 
 Removed
 ~~~~~~~
 
-* Remove legacy menu entries.
-
-Unreleased
-----------
+* Legacy menu integration for AlekSIS-Core pre-3.0
 
 Added
 ~~~~~
 
-* Add SPA support.
+* Add SPA support for AlekSIS-Core 3.0
 
 Changed
 ~~~~~~~
@@ -302,3 +317,5 @@ Fixed
 .. _2.0.1: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/2.0.1
 .. _2.1: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/2.1
 .. _2.1.1: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/2.1.1
+.. _3.0b0: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/3.0b0
+.. _3.0: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/3.0
diff --git a/aleksis/apps/alsijil/frontend/index.js b/aleksis/apps/alsijil/frontend/index.js
index be247a7bd650df9a3cc5cc491a0d00d0f86ac0fb..521c4b5629d6800abebaec1e4ce27f4f57cc3ab4 100644
--- a/aleksis/apps/alsijil/frontend/index.js
+++ b/aleksis/apps/alsijil/frontend/index.js
@@ -1,396 +1,396 @@
-import { notLoggedInValidator, hasPersonValidator } from "aleksis.core/routeValidators";
+import {
+  notLoggedInValidator,
+  hasPersonValidator,
+} from "aleksis.core/routeValidators";
 
-export default
-  {
-    meta: {
-      inMenu: true,
-      titleKey: "alsijil.menu_title",
-      icon: "mdi-account-group-outline",
-      validators: [
-        hasPersonValidator
-      ]
-    },
-    props: {
-      byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-    },
-    children: [
-      {
-        path: "lesson",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.lessonPeriod",
-        meta: {
-          inMenu: true,
-          titleKey: "alsijil.lesson.menu_title",
-          icon: "mdi-alarm",
-          permission: "alsijil.view_lesson_menu_rule",
-        },
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "lesson/:year(\\d+)/:week(\\d+)/:id_(\\d+)",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.lessonPeriodByCWAndID",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "extra_lesson/:id_(\\d+)/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.extraLessonByID",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "event/:id_(\\d+)/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.eventByID",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "week/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.weekView",
-        meta: {
-          inMenu: true,
-          titleKey: "alsijil.week.menu_title",
-          icon: "mdi-view-week-outline",
-          permission: "alsijil.view_week_menu_rule",
-        },
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "week/:year(\\d+)/:week(\\d+)/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.weekViewByWeek",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "week/year/cw/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.weekViewPlaceholders",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "week/:type_/:id_(\\d+)/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.weekViewByTypeAndID",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "week/year/cw/:type_/:id_(\\d+)/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.weekViewPlaceholdersByTypeAndID",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "week/:year(\\d+)/:week(\\d+)/:type_/:id_(\\d+)/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.weekViewByWeekTypeAndID",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "print/group/:id_(\\d+)",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.fullRegisterGroup",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "groups/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.myGroups",
-        meta: {
-          inMenu: true,
-          titleKey: "alsijil.groups.menu_title",
-          icon: "mdi-account-multiple-outline",
-          permission: "alsijil.view_my_groups_rule",
-        },
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "groups/:pk(\\d+)/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.studentsList",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "persons/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.myStudents",
-        meta: {
-          inMenu: true,
-          titleKey: "alsijil.persons.menu_title",
-          icon: "mdi-account-school-outline",
-          permission: "alsijil.view_my_students_rule",
-        },
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "persons/:id_(\\d+)/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.overviewPerson",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "me/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.overviewMe",
-        meta: {
-          inMenu: true,
-          titleKey: "alsijil.my_overview.menu_title",
-          icon: "mdi-chart-box-outline",
-          permission: "alsijil.view_person_overview_menu_rule",
-        },
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "notes/:pk(\\d+)/delete/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.deletePersonalNote",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "absence/new/:id_(\\d+)/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.registerAbsenceWithID",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "absence/new/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.registerAbsence",
-        meta: {
-          inMenu: true,
-          titleKey: "alsijil.absence.menu_title",
-          icon: "mdi-message-alert-outline",
-          permission: "alsijil.view_register_absence_rule",
-        },
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "extra_marks/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.extraMarks",
-        meta: {
-          inMenu: true,
-          titleKey: "alsijil.extra_marks.menu_title",
-          icon: "mdi-label-variant-outline",
-          permission: "alsijil.view_extramarks_rule",
-        },
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "extra_marks/create/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.createExtraMark",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "extra_marks/:pk(\\d+)/edit/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.editExtraMark",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "extra_marks/:pk(\\d+)/delete/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.deleteExtraMark",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "excuse_types/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.excuseTypes",
-        meta: {
-          inMenu: true,
-          titleKey: "alsijil.excuse_types.menu_title",
-          icon: "mdi-label-outline",
-          permission: "alsijil.view_excusetypes_rule",
-        },
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "excuse_types/create/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.createExcuseType",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "excuse_types/:pk(\\d+)/edit/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.editExcuseType",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "excuse_types/:pk(\\d+)/delete/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.deleteExcuseType",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "group_roles/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.groupRoles",
-        meta: {
-          inMenu: true,
-          titleKey: "alsijil.group_roles.menu_title_manage",
-          icon: "mdi-clipboard-plus-outline",
-          permission: "alsijil.view_grouproles_rule",
-        },
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "group_roles/create/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.createGroupRole",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "group_roles/:pk(\\d+)/edit/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.editGroupRole",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "group_roles/:pk(\\d+)/delete/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.deleteGroupRole",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "groups/:pk(\\d+)/group_roles/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.assignedGroupRoles",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "groups/:pk(\\d+)/group_roles/assign/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.assignGroupRole",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "groups/:pk(\\d+)/group_roles/:role_pk(\\d+)/assign/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.assignGroupRoleByRolePK",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "group_roles/assignments/:pk(\\d+)/edit/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.editGroupRoleAssignment",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "group_roles/assignments/:pk(\\d+)/stop/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.stopGroupRoleAssignment",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "group_roles/assignments/:pk(\\d+)/delete/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.deleteGroupRoleAssignment",
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "group_roles/assignments/assign/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.assignGroupRoleMultiple",
-        meta: {
-          inMenu: true,
-          titleKey: "alsijil.group_roles.menu_title_assign",
-          icon: "mdi-clipboard-account-outline",
-          permission: "alsijil.assign_grouprole_for_multiple_rule",
-        },
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      {
-        path: "all/",
-        component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
-        name: "alsijil.allRegisterObjects",
-        meta: {
-          inMenu: true,
-          titleKey: "alsijil.all_lessons.menu_title",
-          icon: "mdi-format-list-text",
-          permission: "alsijil.view_register_objects_list_rule",
-        },
-        props: {
-          byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
-        },
-      },
-      ],
-  }
+export default {
+  meta: {
+    inMenu: true,
+    titleKey: "alsijil.menu_title",
+    icon: "mdi-account-group-outline",
+    validators: [hasPersonValidator],
+  },
+  props: {
+    byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+  },
+  children: [
+    {
+      path: "lesson",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.lessonPeriod",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.lesson.menu_title",
+        icon: "mdi-alarm",
+        permission: "alsijil.view_lesson_menu_rule",
+      },
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "lesson/:year(\\d+)/:week(\\d+)/:id_(\\d+)",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.lessonPeriodByCWAndID",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "extra_lesson/:id_(\\d+)/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.extraLessonByID",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "event/:id_(\\d+)/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.eventByID",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "week/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.weekView",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.week.menu_title",
+        icon: "mdi-view-week-outline",
+        permission: "alsijil.view_week_menu_rule",
+      },
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "week/:year(\\d+)/:week(\\d+)/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.weekViewByWeek",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "week/year/cw/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.weekViewPlaceholders",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "week/:type_/:id_(\\d+)/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.weekViewByTypeAndID",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "week/year/cw/:type_/:id_(\\d+)/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.weekViewPlaceholdersByTypeAndID",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "week/:year(\\d+)/:week(\\d+)/:type_/:id_(\\d+)/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.weekViewByWeekTypeAndID",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "print/group/:id_(\\d+)",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.fullRegisterGroup",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "groups/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.myGroups",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.groups.menu_title",
+        icon: "mdi-account-multiple-outline",
+        permission: "alsijil.view_my_groups_rule",
+      },
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "groups/:pk(\\d+)/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.studentsList",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "persons/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.myStudents",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.persons.menu_title",
+        icon: "mdi-account-school-outline",
+        permission: "alsijil.view_my_students_rule",
+      },
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "persons/:id_(\\d+)/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.overviewPerson",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "me/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.overviewMe",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.my_overview.menu_title",
+        icon: "mdi-chart-box-outline",
+        permission: "alsijil.view_person_overview_menu_rule",
+      },
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "notes/:pk(\\d+)/delete/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.deletePersonalNote",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "absence/new/:id_(\\d+)/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.registerAbsenceWithID",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "absence/new/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.registerAbsence",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.absence.menu_title",
+        icon: "mdi-message-alert-outline",
+        permission: "alsijil.view_register_absence_rule",
+      },
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "extra_marks/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.extraMarks",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.extra_marks.menu_title",
+        icon: "mdi-label-variant-outline",
+        permission: "alsijil.view_extramarks_rule",
+      },
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "extra_marks/create/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.createExtraMark",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "extra_marks/:pk(\\d+)/edit/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.editExtraMark",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "extra_marks/:pk(\\d+)/delete/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.deleteExtraMark",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "excuse_types/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.excuseTypes",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.excuse_types.menu_title",
+        icon: "mdi-label-outline",
+        permission: "alsijil.view_excusetypes_rule",
+      },
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "excuse_types/create/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.createExcuseType",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "excuse_types/:pk(\\d+)/edit/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.editExcuseType",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "excuse_types/:pk(\\d+)/delete/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.deleteExcuseType",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "group_roles/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.groupRoles",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.group_roles.menu_title_manage",
+        icon: "mdi-clipboard-plus-outline",
+        permission: "alsijil.view_grouproles_rule",
+      },
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "group_roles/create/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.createGroupRole",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "group_roles/:pk(\\d+)/edit/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.editGroupRole",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "group_roles/:pk(\\d+)/delete/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.deleteGroupRole",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "groups/:pk(\\d+)/group_roles/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.assignedGroupRoles",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "groups/:pk(\\d+)/group_roles/assign/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.assignGroupRole",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "groups/:pk(\\d+)/group_roles/:role_pk(\\d+)/assign/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.assignGroupRoleByRolePK",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "group_roles/assignments/:pk(\\d+)/edit/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.editGroupRoleAssignment",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "group_roles/assignments/:pk(\\d+)/stop/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.stopGroupRoleAssignment",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "group_roles/assignments/:pk(\\d+)/delete/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.deleteGroupRoleAssignment",
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "group_roles/assignments/assign/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.assignGroupRoleMultiple",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.group_roles.menu_title_assign",
+        icon: "mdi-clipboard-account-outline",
+        permission: "alsijil.assign_grouprole_for_multiple_rule",
+      },
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+    {
+      path: "all/",
+      component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
+      name: "alsijil.allRegisterObjects",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.all_lessons.menu_title",
+        icon: "mdi-format-list-text",
+        permission: "alsijil.view_register_objects_list_rule",
+      },
+      props: {
+        byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
+      },
+    },
+  ],
+};
diff --git a/aleksis/apps/alsijil/frontend/messages/de.json b/aleksis/apps/alsijil/frontend/messages/de.json
index 0f754c7664cfffcea1dc1d8008d03a1fd7941d65..09456c74481a13e390b1e08e2389d027d8c6974d 100644
--- a/aleksis/apps/alsijil/frontend/messages/de.json
+++ b/aleksis/apps/alsijil/frontend/messages/de.json
@@ -1,36 +1,36 @@
 {
-    "alsijil": {
-        "menu_title": "Klassenbuch",
-        "lesson": {
-            "menu_title": "Aktuelle Unterrichtsstunde"
-        },
-        "week": {
-            "menu_title": "Aktuelle Woche"
-        },
-        "groups": {
-            "menu_title": "Meine Gruppen"
-        },
-        "persons": {
-            "menu_title": "Meine Schülerinnen und Schüler"
-        },
-        "absence": {
-            "menu_title": "Abwesenheit eintragen"
-        },
-        "my_overview": {
-            "menu_title": "Meine Ãœbersicht"
-        },
-        "excuse_types": {
-            "menu_title": "Entschuldigungsarten"
-        },
-        "group_roles": {
-            "menu_title_assign": "Gruppenrollen zuweisen",
-            "menu_title_manage": "Gruppenrollen verwalten"
-        },
-        "extra_marks": {
-            "menu_title": "Zusätzliche Markierungen"
-        },
-        "all_lessons": {
-            "menu_title": "Alle Stunden"
-        }
+  "alsijil": {
+    "menu_title": "Klassenbuch",
+    "lesson": {
+      "menu_title": "Aktuelle Unterrichtsstunde"
+    },
+    "week": {
+      "menu_title": "Aktuelle Woche"
+    },
+    "groups": {
+      "menu_title": "Meine Gruppen"
+    },
+    "persons": {
+      "menu_title": "Meine Schülerinnen und Schüler"
+    },
+    "absence": {
+      "menu_title": "Abwesenheit eintragen"
+    },
+    "my_overview": {
+      "menu_title": "Meine Ãœbersicht"
+    },
+    "excuse_types": {
+      "menu_title": "Entschuldigungsarten"
+    },
+    "group_roles": {
+      "menu_title_assign": "Gruppenrollen zuweisen",
+      "menu_title_manage": "Gruppenrollen verwalten"
+    },
+    "extra_marks": {
+      "menu_title": "Zusätzliche Markierungen"
+    },
+    "all_lessons": {
+      "menu_title": "Alle Stunden"
     }
+  }
 }
diff --git a/aleksis/apps/alsijil/frontend/messages/ru.json b/aleksis/apps/alsijil/frontend/messages/ru.json
index 26036c9ce82cfc8e9a3bc04b71c5ffdc1a2d6543..8fb7aea426577ea2406c1579dbe44b39b70e5ef6 100644
--- a/aleksis/apps/alsijil/frontend/messages/ru.json
+++ b/aleksis/apps/alsijil/frontend/messages/ru.json
@@ -1,36 +1,36 @@
 {
-    "alsijil": {
-        "my_overview": {
-            "menu_title": "Мой обзор"
-        },
-        "group_roles": {
-            "menu_title_manage": "Управление ролями групп",
-            "menu_title_assign": "Назначить роль группы"
-        },
-        "all_lessons": {
-            "menu_title": "Все уроки"
-        },
-        "menu_title": "Классный журнал",
-        "lesson": {
-            "menu_title": "Текущий урок"
-        },
-        "week": {
-            "menu_title": "Текущая неделя"
-        },
-        "groups": {
-            "menu_title": "Мои группы"
-        },
-        "persons": {
-            "menu_title": "Мои студенты"
-        },
-        "absence": {
-            "menu_title": "Регистрация отсутствия"
-        },
-        "extra_marks": {
-            "menu_title": "Дополнительные отметки"
-        },
-        "excuse_types": {
-            "menu_title": "Типы объяснительных"
-        }
+  "alsijil": {
+    "my_overview": {
+      "menu_title": "Мой обзор"
+    },
+    "group_roles": {
+      "menu_title_manage": "Управление ролями групп",
+      "menu_title_assign": "Назначить роль группы"
+    },
+    "all_lessons": {
+      "menu_title": "Все уроки"
+    },
+    "menu_title": "Классный журнал",
+    "lesson": {
+      "menu_title": "Текущий урок"
+    },
+    "week": {
+      "menu_title": "Текущая неделя"
+    },
+    "groups": {
+      "menu_title": "Мои группы"
+    },
+    "persons": {
+      "menu_title": "Мои студенты"
+    },
+    "absence": {
+      "menu_title": "Регистрация отсутствия"
+    },
+    "extra_marks": {
+      "menu_title": "Дополнительные отметки"
+    },
+    "excuse_types": {
+      "menu_title": "Типы объяснительных"
     }
+  }
 }
diff --git a/aleksis/apps/alsijil/frontend/messages/uk.json b/aleksis/apps/alsijil/frontend/messages/uk.json
index 8aa5da05e28341d587cb89f8a83e9568fc1e0dfa..a29573bea059ddf91b6ae156be5d76914dd2ca50 100644
--- a/aleksis/apps/alsijil/frontend/messages/uk.json
+++ b/aleksis/apps/alsijil/frontend/messages/uk.json
@@ -1,36 +1,36 @@
 {
-    "alsijil": {
-        "week": {
-            "menu_title": "Поточний тиждень"
-        },
-        "groups": {
-            "menu_title": "Мої групи"
-        },
-        "persons": {
-            "menu_title": "Мої студенти"
-        },
-        "absence": {
-            "menu_title": "Реєстрація відсутності"
-        },
-        "my_overview": {
-            "menu_title": "Мій огляд"
-        },
-        "extra_marks": {
-            "menu_title": "Додаткові відмітки"
-        },
-        "excuse_types": {
-            "menu_title": "Типи пояснень"
-        },
-        "group_roles": {
-            "menu_title_manage": "Керування ролями групи",
-            "menu_title_assign": "Призначити роль групи"
-        },
-        "all_lessons": {
-            "menu_title": "Усі уроки"
-        },
-        "menu_title": "Класний журнал",
-        "lesson": {
-            "menu_title": "Поточний урок"
-        }
+  "alsijil": {
+    "week": {
+      "menu_title": "Поточний тиждень"
+    },
+    "groups": {
+      "menu_title": "Мої групи"
+    },
+    "persons": {
+      "menu_title": "Мої студенти"
+    },
+    "absence": {
+      "menu_title": "Реєстрація відсутності"
+    },
+    "my_overview": {
+      "menu_title": "Мій огляд"
+    },
+    "extra_marks": {
+      "menu_title": "Додаткові відмітки"
+    },
+    "excuse_types": {
+      "menu_title": "Типи пояснень"
+    },
+    "group_roles": {
+      "menu_title_manage": "Керування ролями групи",
+      "menu_title_assign": "Призначити роль групи"
+    },
+    "all_lessons": {
+      "menu_title": "Усі уроки"
+    },
+    "menu_title": "Класний журнал",
+    "lesson": {
+      "menu_title": "Поточний урок"
     }
+  }
 }
diff --git a/aleksis/apps/alsijil/model_extensions.py b/aleksis/apps/alsijil/model_extensions.py
index 60504010d83ddcedb7a73646a28a2eb421e2fa62..8a92d667106efd853b225f946c4139b0d5799ef6 100644
--- a/aleksis/apps/alsijil/model_extensions.py
+++ b/aleksis/apps/alsijil/model_extensions.py
@@ -482,7 +482,7 @@ def generate_person_list_with_class_register_statistics(
         persons = persons.annotate(
             **{
                 excuse_type.count_label: Count(
-                    "filtered_personal_notes__absent",
+                    "filtered_personal_notes",
                     filter=Q(
                         filtered_personal_notes__absent=True,
                         filtered_personal_notes__excuse_type=excuse_type,
diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py
index b504a294164a6388099148fda5149091767577a7..c9a96471458405350f0d0a91eafe133d72d317c0 100644
--- a/aleksis/apps/alsijil/models.py
+++ b/aleksis/apps/alsijil/models.py
@@ -66,7 +66,7 @@ class ExcuseType(ExtensibleModel):
 
     @property
     def count_label(self):
-        return f"{self.short_name}_count"
+        return f"excuse_type_{self.id}_count"
 
     class Meta:
         ordering = ["name"]
@@ -437,7 +437,7 @@ class ExtraMark(ExtensibleModel):
 
     @property
     def count_label(self):
-        return f"{self.short_name}_count"
+        return f"extra_mark_{self.id}_count"
 
     class Meta:
         ordering = ["short_name"]
diff --git a/aleksis/apps/alsijil/static/css/alsijil/alsijil.css b/aleksis/apps/alsijil/static/css/alsijil/alsijil.css
index 3e14ca014661f0f80d1df639d02949db6ffb59e9..a30fb99bb8b239ca38f7f4d627329594387741c6 100644
--- a/aleksis/apps/alsijil/static/css/alsijil/alsijil.css
+++ b/aleksis/apps/alsijil/static/css/alsijil/alsijil.css
@@ -9,52 +9,52 @@ table a.tr-link {
 }
 
 .collapsible-icon-right {
-	align-self: end;
-	flex-grow: 100;
-	text-align: right!important;
+  align-self: end;
+  flex-grow: 100;
+  text-align: right !important;
 }
 
 @media only screen and (min-width: 1201px) {
-    .hide-on-extra-large-only {
-        display: none;
-    }
+  .hide-on-extra-large-only {
+    display: none;
+  }
 }
 
 @media only screen and (max-width: 1200px) {
-    .show-on-extra-large {
-        display: none;
-    }
+  .show-on-extra-large {
+    display: none;
+  }
 }
 
 @media only screen and (max-width: 600px) {
-    .collection .collection-item.avatar {
-        padding-left: 20px;
-    }
-    .collection .collection-item.avatar:not(.circle-clipper) > .circle {
-        position: relative;
-        margin-bottom: 10px;
-    }
+  .collection .collection-item.avatar {
+    padding-left: 20px;
+  }
+  .collection .collection-item.avatar:not(.circle-clipper) > .circle {
+    position: relative;
+    margin-bottom: 10px;
+  }
 }
 
 .collapsible li .show-on-active {
-    display: none;
+  display: none;
 }
 
 .collapsible li.active .show-on-active {
-    display: block;
+  display: block;
 }
 
 th.chip-height {
-    height: 67px;
-    line-height: 2.2;
+  height: 67px;
+  line-height: 2.2;
 }
 
 .collection-item.chip-height {
-    height: 52px;
-    line-height: 2.2;
+  height: 52px;
+  line-height: 2.2;
 }
 
 li.collection-item.button-height {
-    height: 58px;
-    line-height: 2.5;
+  height: 58px;
+  line-height: 2.5;
 }
diff --git a/aleksis/apps/alsijil/static/css/alsijil/full_register.css b/aleksis/apps/alsijil/static/css/alsijil/full_register.css
index 9068a104cb2464a0cb95073e3ee8a66bddfaf21e..533c84326887b2050cb66edadcf92706f64b963d 100644
--- a/aleksis/apps/alsijil/static/css/alsijil/full_register.css
+++ b/aleksis/apps/alsijil/static/css/alsijil/full_register.css
@@ -1,72 +1,76 @@
-table.small-print, td.small-print, th.small-print {
-    font-size: 10pt;
+table.small-print,
+td.small-print,
+th.small-print {
+  font-size: 10pt;
 }
 
 #signatures {
-    padding-top: 3em;
+  padding-top: 3em;
 }
 
 #signatures .signature {
-    display: inline-block;
-    width: 12em;
-    border-top: 1px solid;
-    margin-right: 20px;
+  display: inline-block;
+  width: 12em;
+  border-top: 1px solid;
+  margin-right: 20px;
 }
 
 tr {
-    border-bottom: 1px solid rgba(0, 0, 0, 0.3);
+  border-bottom: 1px solid rgba(0, 0, 0, 0.3);
 }
 
-td, th {
-    padding: 1px;
+td,
+th {
+  padding: 1px;
 }
 
 tr.lessons-day-first {
-    border-top: 3px solid rgba(0, 0, 0, 0.3);
+  border-top: 3px solid rgba(0, 0, 0, 0.3);
 }
 
-td.rotate, th.rotate {
-    text-align: center;
-    transform: rotate(-90deg);
+td.rotate,
+th.rotate {
+  text-align: center;
+  transform: rotate(-90deg);
 }
 
 tr.lesson-cancelled td {
-    background-color: #EF9A9A;
+  background-color: #ef9a9a;
 }
 
 tr.lesson-substituted td {
-    background-color: #ffb74d;
+  background-color: #ffb74d;
 }
 
 td.lesson-notes {
-    font-size: 80%;
+  font-size: 80%;
 }
 
 td.lesson-notes span.lesson-note-absent {
-    color: #cc0000;
+  color: #cc0000;
 }
 
 td.lesson-notes span.lesson-note-late {
-    color: #ff9933;
+  color: #ff9933;
 }
 
 td.lesson-notes span.lesson-note-excused {
-    color: #009933;
+  color: #009933;
 }
 
 table.person-info {
-    border: none;
+  border: none;
 }
 
 table.person-info td.person-img {
-    text-align: center;
+  text-align: center;
 }
 
 table.person-info td.person-img img {
-    max-height: 30mm;
+  max-height: 30mm;
 }
 
 img.max-size-600 {
-    max-width: 600px;
-    max-height: 600px;
+  max-width: 600px;
+  max-height: 600px;
 }
diff --git a/aleksis/apps/alsijil/static/css/alsijil/lesson.css b/aleksis/apps/alsijil/static/css/alsijil/lesson.css
index fbfa4d8d683d42b8fe003edc54bf10411fbbc7eb..48937d33d5a763d9d8ac6c4dbdfa4c9eae573ac3 100644
--- a/aleksis/apps/alsijil/static/css/alsijil/lesson.css
+++ b/aleksis/apps/alsijil/static/css/alsijil/lesson.css
@@ -1,137 +1,138 @@
 .alsijil-check-box {
-    margin-right: 10px;
+  margin-right: 10px;
 }
 
 .alsijil-check-box [type="checkbox"] {
-    padding-left: 30px;
+  padding-left: 30px;
 }
 
 .alsijil-lesson-cancelled {
-    text-decoration: line-through;
+  text-decoration: line-through;
 }
 
-.alsijil-tardiness-text{
-    vertical-align: super;
+.alsijil-tardiness-text {
+  vertical-align: super;
 }
 
-
 @media only screen and (max-width: 992px) {
-    .no-mobile-card {
-        border: unset;
-        padding: unset;
-        margin: unset;
-        box-shadow: unset;
-    }
-    .no-mobile-card .card-content {
-        padding: unset;
-    }
-    table.alsijil-table.horizontal-on-small {
-        display: block;
-        max-width: calc(100vw - 40px);
-    }
-    table.alsijil-table.horizontal-on-small thead {
-        display: none;
-    }
-    table.alsijil-table.horizontal-on-small tbody {
-        overflow-x: scroll;
-        display: flex;
-        column-gap: 1rem;
-        flex-wrap: nowrap;
-        align-items: stretch;
-        scroll-snap-type: x proximity;
-    }
-
-    table.alsijil-table.horizontal-on-small tr {
-        flex-basis: min(75vw, 400px);
-        flex-shrink: 0;
-        flex-grow: 1;
-        border-radius: 8px;
-        display: flex;
-        flex-direction: column;
-        justify-content: space-between;
-        scroll-snap-align: center;
-        transition: all .5s;
-        margin: 0.5rem 0 1rem 0;
-        background-color: #fff!important;
-    	box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.12), 0 1px 5px 0 rgba(0, 0, 0, 0.2);
-        padding: 24px;
-    }
-    table.alsijil-table.horizontal-on-small tr:first-of-type {
-        margin-inline-start: .4rem;
-        -moz-margin-start: .4rem;
-        -webkit-margin-start: .4rem;
-    }
-
-    table.alsijil-table.horizontal-on-small tr:last-of-type {
-        margin-inline-end: .4rem;
-        -moz-margin-end: .4rem;
-        -webkit-margin-end: .4rem;
-    }
-    table.alsijil-table.horizontal-on-small td.center-align {
-        text-align: left;
-    }
-    table.alsijil-table.horizontal-on-small .person-name {
-        font-size: 24px;
-        font-weight: 300;
-        display: block;
-        line-height: 32px;
-        margin-bottom: 8px;
-    }
-}
-
-.alsijil-time-head, .alsijil-object-head {
+  .no-mobile-card {
+    border: unset;
+    padding: unset;
+    margin: unset;
+    box-shadow: unset;
+  }
+  .no-mobile-card .card-content {
+    padding: unset;
+  }
+  table.alsijil-table.horizontal-on-small {
+    display: block;
+    max-width: calc(100vw - 40px);
+  }
+  table.alsijil-table.horizontal-on-small thead {
+    display: none;
+  }
+  table.alsijil-table.horizontal-on-small tbody {
+    overflow-x: scroll;
+    display: flex;
+    column-gap: 1rem;
+    flex-wrap: nowrap;
+    align-items: stretch;
+    scroll-snap-type: x proximity;
+  }
+
+  table.alsijil-table.horizontal-on-small tr {
+    flex-basis: min(75vw, 400px);
+    flex-shrink: 0;
+    flex-grow: 1;
+    border-radius: 8px;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    scroll-snap-align: center;
+    transition: all 0.5s;
+    margin: 0.5rem 0 1rem 0;
+    background-color: #fff !important;
+    box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
+      0 3px 1px -2px rgba(0, 0, 0, 0.12), 0 1px 5px 0 rgba(0, 0, 0, 0.2);
+    padding: 24px;
+  }
+  table.alsijil-table.horizontal-on-small tr:first-of-type {
+    margin-inline-start: 0.4rem;
+    -moz-margin-start: 0.4rem;
+    -webkit-margin-start: 0.4rem;
+  }
+
+  table.alsijil-table.horizontal-on-small tr:last-of-type {
+    margin-inline-end: 0.4rem;
+    -moz-margin-end: 0.4rem;
+    -webkit-margin-end: 0.4rem;
+  }
+  table.alsijil-table.horizontal-on-small td.center-align {
+    text-align: left;
+  }
+  table.alsijil-table.horizontal-on-small .person-name {
+    font-size: 24px;
+    font-weight: 300;
     display: block;
+    line-height: 32px;
+    margin-bottom: 8px;
+  }
+}
+
+.alsijil-time-head,
+.alsijil-object-head {
+  display: block;
 }
 
 .alsijil-time-head {
-    font-size: 2rem;
-    line-height: 1.1;
+  font-size: 2rem;
+  line-height: 1.1;
 }
 
 .alsijil-object-head {
-    font-size: 3rem;
+  font-size: 3rem;
 }
 
 @media only screen and (max-width: 600px) {
-    .alsijil-time-head {
-        font-size: 1.5rem;
-    }
+  .alsijil-time-head {
+    font-size: 1.5rem;
+  }
 
-    .alsijil-object-head {
-        font-size: 2.2rem;
-        line-height: 1.4;
-    }
+  .alsijil-object-head {
+    font-size: 2.2rem;
+    line-height: 1.4;
+  }
 }
 
 .alsijil-nav {
-    line-height: 36px;
+  line-height: 36px;
 }
 
 .alsijil-header-nav-button {
-    height: 66px;
-    padding: 0;
+  height: 66px;
+  padding: 0;
 }
 
 .alsijil-header-nav-button.left {
-    margin-right: 5px;
+  margin-right: 5px;
 }
 
 .alsijil-header-nav-button.right {
-    margin-left: 5px;
+  margin-left: 5px;
 }
 
 .alsijil-header-nav-button i.material-icons {
-    line-height: 60px;
-    height: 60px;
-    font-size: 40px;
+  line-height: 60px;
+  height: 60px;
+  font-size: 40px;
 }
 
 .alsijil-nav-header {
-    width: calc(100% + 40px);
-    padding: 10px 20px;
-    margin: -10px -20px 0;
+  width: calc(100% + 40px);
+  padding: 10px 20px;
+  margin: -10px -20px 0;
 }
 
 .tabs-icons .tab svg.iconify {
-    display: block;
+  display: block;
 }
diff --git a/aleksis/apps/alsijil/static/css/alsijil/person.css b/aleksis/apps/alsijil/static/css/alsijil/person.css
index b5a59aae95235f696a06d0641215b1392dd5b2a3..d385d7b69e031fafd4c21d037b70b8f973e99b74 100644
--- a/aleksis/apps/alsijil/static/css/alsijil/person.css
+++ b/aleksis/apps/alsijil/static/css/alsijil/person.css
@@ -1,101 +1,100 @@
 span.input-field.inline > .select-wrapper > input {
-    color: red;
-    padding: 14px 0 0 0;
-    line-height: 2px;
-    height: 36px;
-    vertical-align: middle;
+  color: red;
+  padding: 14px 0 0 0;
+  line-height: 2px;
+  height: 36px;
+  vertical-align: middle;
 }
 
 span.input-field.inline > .select-wrapper .caret {
-    top: 12px !important;
+  top: 12px !important;
 }
 
 @media screen and (min-width: 1400px) {
-    li.collection-item form {
-        margin: -30px 0 -30px 0;
-    }
+  li.collection-item form {
+    margin: -30px 0 -30px 0;
+  }
 
-    li.collection-item#title #select_all_span {
-        margin-top: 5px;
-    }
+  li.collection-item#title #select_all_span {
+    margin-top: 5px;
+  }
 }
 
 .collection {
-    overflow: visible;
-    overflow-x: hidden;
+  overflow: visible;
+  overflow-x: hidden;
 }
 
 #select_all_container {
-    display: none;
+  display: none;
 }
 
 #select_all_box:indeterminate + span:not(.lever):before {
-    top: -4px;
-    left: -6px;
-    width: 10px;
-    height: 12px;
-    border-top: none;
-    border-left: none;
-    border-right: white 2px solid;
-    border-bottom: none;
-    transform: rotate(90deg);
-    backface-visibility: hidden;
-    transform-origin: 100% 100%;
-
+  top: -4px;
+  left: -6px;
+  width: 10px;
+  height: 12px;
+  border-top: none;
+  border-left: none;
+  border-right: white 2px solid;
+  border-bottom: none;
+  transform: rotate(90deg);
+  backface-visibility: hidden;
+  transform-origin: 100% 100%;
 }
 
 #select_all_box:indeterminate + span:not(.lever):after {
-    top: 0;
-    width: 20px;
-    height: 20px;
-    border: 2px solid currentColor;
-    background-color: currentColor;
-    z-index: 0;
+  top: 0;
+  width: 20px;
+  height: 20px;
+  border: 2px solid currentColor;
+  background-color: currentColor;
+  z-index: 0;
 }
 
 #select_all_box_text {
-    color: #9e9e9e !important;
+  color: #9e9e9e !important;
 }
 
 td.material-icons {
-    display: table-cell;
+  display: table-cell;
 }
 
 .medium-high {
-    position: relative;
-    top: 50%;
-    left: 50%;
-    transform: translate(-50%, 50%);
+  position: relative;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, 50%);
 }
 
 @media screen and (min-width: 600px) {
-    /* On medium and up devices */
-    .medium-high-right {
-        float: right;
-        transform: translate(0%, 50%);
-    }
+  /* On medium and up devices */
+  .medium-high-right {
+    float: right;
+    transform: translate(0%, 50%);
+  }
 }
 
 @media screen and (max-width: 600px) {
-    /* Only on small devices */
-    .full-width-s {
-        width: 100%;
-    }
-
-    #heading {
-        display: block;
-    }
-    #heading + a {
-        float: none!important;
-    }
+  /* Only on small devices */
+  .full-width-s {
+    width: 100%;
+  }
+
+  #heading {
+    display: block;
+  }
+  #heading + a {
+    float: none !important;
+  }
 }
 
 .overflow-x-scroll {
-    overflow-x: scroll;
+  overflow-x: scroll;
 }
 
 figure.modal-content figcaption {
-    font-weight: 300;
-    font-size: 2.28rem;
-    line-height: 110%;
+  font-weight: 300;
+  font-size: 2.28rem;
+  line-height: 110%;
 }
diff --git a/aleksis/apps/alsijil/static/css/alsijil/week_view.css b/aleksis/apps/alsijil/static/css/alsijil/week_view.css
index ccd1595e0fa5ad3f3c10ee8857c644723892e6f2..a42111f55e31d3b3f518873218c76fd246106afe 100644
--- a/aleksis/apps/alsijil/static/css/alsijil/week_view.css
+++ b/aleksis/apps/alsijil/static/css/alsijil/week_view.css
@@ -1,116 +1,122 @@
 @media screen and (max-width: 600px) {
-    #toggle-row button[type=submit] {
-        width: 100%;
-        margin-bottom: 1em;
-    }
+  #toggle-row button[type="submit"] {
+    width: 100%;
+    margin-bottom: 1em;
+  }
 }
 
 .horizontal-scroll-container {
-    overflow-x: scroll;
-    display: flex;
-    column-gap: 1rem;
-    flex-wrap: nowrap;
-    align-items: stretch;
-    scroll-snap-type: x proximity;
+  overflow-x: scroll;
+  display: flex;
+  column-gap: 1rem;
+  flex-wrap: nowrap;
+  align-items: stretch;
+  scroll-snap-type: x proximity;
 }
 
 .horizontal-scroll-container.vertical {
-    flex-wrap: wrap;
-    overflow-x: inherit;
+  flex-wrap: wrap;
+  overflow-x: inherit;
 }
 
 .horizontal-scroll-container.vertical .horizontal-scroll-card {
-    margin-inline: 0;
+  margin-inline: 0;
 }
 
 dl {
-    margin: 0;
-    padding: 0;
+  margin: 0;
+  padding: 0;
 }
 
 dt {
-    font-weight: bold;
+  font-weight: bold;
 }
 
 dd {
-    margin: 0;
-    padding: unset;
+  margin: 0;
+  padding: unset;
 }
 
 .horizontal-scroll-card {
-    flex-basis: min(75vw, 400px);
-    flex-shrink: 0;
-    flex-grow: 1;
-    border-radius: 8px;
-    display: flex;
-    flex-direction: column;
-    justify-content: space-between;
-    scroll-snap-align: center;
-    transition: all .5s;
+  flex-basis: min(75vw, 400px);
+  flex-shrink: 0;
+  flex-grow: 1;
+  border-radius: 8px;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  scroll-snap-align: center;
+  transition: all 0.5s;
 }
 
 .horizontal-scroll-card:first-of-type {
-    margin-inline-start: .4rem;
-    -moz-margin-start: .4rem;
-    -webkit-margin-start: .4rem;
+  margin-inline-start: 0.4rem;
+  -moz-margin-start: 0.4rem;
+  -webkit-margin-start: 0.4rem;
 }
 
 .horizontal-scroll-card:last-of-type {
-    margin-inline-end: .4rem;
-    -moz-margin-end: .4rem;
-    -webkit-margin-end: .4rem;
+  margin-inline-end: 0.4rem;
+  -moz-margin-end: 0.4rem;
+  -webkit-margin-end: 0.4rem;
 }
 
 .horizontal-scroll-card .card-action {
-    margin-bottom: 5px;
+  margin-bottom: 5px;
 }
 
 .horizontal-scroll-card .card-content .card-title {
-    display: flex;
-    justify-content: space-between;
+  display: flex;
+  justify-content: space-between;
 }
 
 .horizontal-scroll-card .card-content .card-title .subject {
-    flex-grow: 5;
+  flex-grow: 5;
 }
 
 .horizontal-scroll-card .one-line {
-   display: grid;
-    grid-auto-flow: column;
-    grid-template-rows: 1fr 1fr;
+  display: grid;
+  grid-auto-flow: column;
+  grid-template-rows: 1fr 1fr;
 }
 
 p.subtitle {
-    display: flex;
-    justify-content: space-between;
-    align-items: flex-end;
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-end;
 }
 
 .btn-superflat ~ span {
-    line-height: 24px;
+  line-height: 24px;
 }
 
-.btn-superflat, .btn-superflat:focus, .btn-superflat:active {
-    border: none;
-    line-height: 1;
-    height: 24px;
-    background: none;
-    font-weight: normal;
+.btn-superflat,
+.btn-superflat:focus,
+.btn-superflat:active {
+  border: none;
+  line-height: 1;
+  height: 24px;
+  background: none;
+  font-weight: normal;
 }
 
 .btn-superflat i.material-icons {
-    vertical-align: middle;
+  vertical-align: middle;
 }
 
 .btn-superflat:hover {
-    cursor: pointer;
+  cursor: pointer;
 }
 
 .unfold-trigger i.material-icons {
-    transition: transform .5s 0s ease-in-out;
-    transform: rotate(-90deg);
+  transition: transform 0.5s 0s ease-in-out;
+  transform: rotate(-90deg);
 }
 
 .unfold-trigger.vertical i.material-icons {
-    transform: rotate(-180deg);
+  transform: rotate(-180deg);
+}
+
+.tabs-icons .tab svg.iconify {
+  display: block;
 }
diff --git a/aleksis/apps/alsijil/static/js/alsijil/week_view.js b/aleksis/apps/alsijil/static/js/alsijil/week_view.js
index c5daa0451b63a63aac4e8b7703beaaa6d8b26fe5..69124b9c41e656948bbced89b06b8d4edaf3b3c2 100644
--- a/aleksis/apps/alsijil/static/js/alsijil/week_view.js
+++ b/aleksis/apps/alsijil/static/js/alsijil/week_view.js
@@ -1,21 +1,20 @@
 $(document).ready(function () {
-    $("#id_group").change(function () {
-        $("#id_teacher").val("").formSelect();
-    });
-    $("#id_teacher").change(function () {
-        $("#id_group").val("").formSelect();
-    });
-    $("#toggle-row.pre-hidden").hide();
-
+  $("#id_group").change(function () {
+    $("#id_teacher").val("").formSelect();
+  });
+  $("#id_teacher").change(function () {
+    $("#id_group").val("").formSelect();
+  });
+  $("#toggle-row.pre-hidden").hide();
 });
 $("#toggle-button").click(function () {
-    $("#toggle-row").toggle();
-})
+  $("#toggle-row").toggle();
+});
 $(".unfold-trigger").click(function (event) {
-    let target = event.target;
-    target.classList.toggle("vertical");
-    let next_container = $(target).parent().next(".horizontal-scroll-container");
-    if (next_container.length >= 1) {
-        next_container[0].classList.toggle("vertical");
-    }
-})
\ No newline at end of file
+  let target = event.target;
+  target.classList.toggle("vertical");
+  let next_container = $(target).parent().next(".horizontal-scroll-container");
+  if (next_container.length >= 1) {
+    next_container[0].classList.toggle("vertical");
+  }
+});
diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html b/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html
index ce38ee671b6e51a9f4417cb8507dc530b2b81a48..d8c8273092fee51086fac6b24599da95fef486d0 100644
--- a/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html
+++ b/aleksis/apps/alsijil/templates/alsijil/class_register/week_view.html
@@ -11,35 +11,6 @@
   <link rel="stylesheet" href="{% static 'css/alsijil/week_view.css' %}"/>
 {% endblock %}
 
-{% block nav_content %}
-  {% if lesson_periods %}
-    <div class="">
-      <ul class="tabs tabs-transparent tabs-icons tabs-fixed-width">
-        <li class="tab col">
-          <a class="active" href="#week-overview">
-            <i class="material-icons iconify" data-icon="mdi:message-bulleted"></i>
-            {% trans "Lesson documentations" %}
-          </a>
-        </li>
-        <li class="tab col">
-          <a href="#personal-notes">
-            <i class="material-icons iconify" data-icon="mdi:account-multiple-outline"></i>
-            {% trans "Persons" %}
-          </a>
-        </li>
-        {% if group_roles %}
-          <li class="tab col">
-            <a href="#group-roles">
-              <i class="material-icons iconify" data-icon="mdi:clipboard-account-outline"></i>
-              {% trans "Group roles" %}
-            </a>
-          </li>
-        {% endif %}
-      </ul>
-    </div>
-  {% endif %}
-{% endblock %}
-
 {% block content %}
   <script type="text/javascript" src="{% static "js/helper.js" %}"></script>
   {{ week_select|json_script:"week_select" }}
@@ -97,6 +68,33 @@
     </p>
   {% endif %}
 
+  {% if lesson_periods %}
+  <div class="col s12 margin-bottom">
+    <ul class="tabs tabs-icons tabs-fixed-width">
+      <li class="tab col">
+        <a class="active" href="#week-overview">
+          <i class="material-icons iconify" data-icon="mdi:message-bulleted"></i>
+          {% trans "Lesson documentations" %}
+        </a>
+      </li>
+      <li class="tab col">
+        <a href="#personal-notes">
+          <i class="material-icons iconify" data-icon="mdi:account-multiple-outline"></i>
+          {% trans "Persons" %}
+        </a>
+      </li>
+      {% if group_roles %}
+        <li class="tab col">
+          <a href="#group-roles">
+            <i class="material-icons iconify" data-icon="mdi:clipboard-account-outline"></i>
+            {% trans "Group roles" %}
+          </a>
+        </li>
+      {% endif %}
+    </ul>
+  </div>
+  {% endif %}
+
   {% if lesson_periods %}
     <div class="row">
       <div class="col s12" id="week-overview">
diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/persons_with_stats.html b/aleksis/apps/alsijil/templates/alsijil/partials/persons_with_stats.html
index bb60dbbd0984d1dab0130ada052cb423288aee2e..69ce9c6c9a011744eb2b3bed80228fe3147ff01b 100644
--- a/aleksis/apps/alsijil/templates/alsijil/partials/persons_with_stats.html
+++ b/aleksis/apps/alsijil/templates/alsijil/partials/persons_with_stats.html
@@ -12,11 +12,13 @@
     <th rowspan="2">{% trans "Name" %}</th>
     <th rowspan="2">{% trans "Primary group" %}</th>
     <th colspan="{{ excuse_types.count|add:4 }}">{% trans "Absences" %}</th>
-    <th colspan="{{ excuse_types_not_absent.count }}">{% trans "Uncounted Absences" %}</th>
-    <th rowspan="2">{% trans "Tardiness" %}</th>
+    {% if excuse_types_not_absent %}
+      <th colspan="{{ excuse_types_not_absent.count }}">{% trans "Uncounted Absences" %}</th>
+    {% endif %}
     {% if extra_marks %}
       <th colspan="{{ extra_marks.count }}">{% trans "Extra marks" %}</th>
     {% endif %}
+    <th rowspan="2">{% trans "Tardiness" %}</th>
     <th rowspan="2"></th>
   </tr>
   <tr class="hide-on-large-only">
@@ -36,12 +38,12 @@
         ({{ excuse_type.short_name }})
       </th>
     {% endfor %}
-    <th class="truncate chip-height">{% trans "Tardiness" %}</th>
     {% for extra_mark in extra_marks %}
       <th class="chip-height">
         {{ extra_mark.short_name }}
       </th>
     {% endfor %}
+    <th class="truncate chip-height">{% trans "Tardiness" %}</th>
     <th rowspan="2"></th>
   </tr>
   <tr class="hide-on-med-and-down">
@@ -110,12 +112,6 @@
           </span>
         </td>
       {% endfor %}
-      <td>
-        <span class="chip orange white-text" title="{% trans "Tardiness" %}">
-          {% firstof person.tardiness|to_time|time:"H\h i\m"  "–" %}
-        </span>
-        <span class="chip orange white-text" title="{% trans "Count of tardiness" %}">{{ person.tardiness_count }} &times;</span>
-      </td>
       {% for extra_mark in extra_marks %}
         <td>
           <span class="chip grey white-text" title="{{ extra_mark.name }}">
@@ -123,6 +119,12 @@
           </span>
         </td>
       {% endfor %}
+      <td>
+        <span class="chip orange white-text" title="{% trans "Tardiness" %}">
+          {% firstof person.tardiness|to_time|time:"H\h i\m"  "–" %}
+        </span>
+        <span class="chip orange white-text" title="{% trans "Count of tardiness" %}">{{ person.tardiness_count }} &times;</span>
+      </td>
 
       <td>
         <a class="btn primary waves-effect waves-light" href="{% url "overview_person" person.pk %}">
diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py
index 7cee4ac8d7b389cd4e838d992ef68750e31386d5..589f194fc06da1217b1b2a4918236fb3c7f6b6df 100644
--- a/aleksis/apps/alsijil/views.py
+++ b/aleksis/apps/alsijil/views.py
@@ -41,7 +41,7 @@ from aleksis.core.mixins import (
 from aleksis.core.models import Group, PDFFile, Person, SchoolTerm
 from aleksis.core.util import messages
 from aleksis.core.util.celery_progress import render_progress_page
-from aleksis.core.util.core_helpers import get_site_preferences, objectgetter_optional
+from aleksis.core.util.core_helpers import get_site_preferences, has_person, objectgetter_optional
 from aleksis.core.util.predicates import check_global_permission
 
 from .filters import PersonalNoteFilter
@@ -642,8 +642,11 @@ def full_register_group(request: HttpRequest, id_: int) -> HttpResponse:
     group = get_object_or_404(Group, pk=id_)
 
     file_object = PDFFile.objects.create()
+    if has_person(request):
+        file_object.person = request.user.person
+        file_object.save()
 
-    redirect_url = reverse("redirect_to_pdf_file", args=[file_object.pk])
+    redirect_url = f"/pdfs/{file_object.pk}"
 
     result = generate_full_register_printout.delay(group.pk, file_object.pk)
 
@@ -883,7 +886,9 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp
         stats = []
         for school_term in school_terms:
             stat = {}
-            personal_notes = PersonalNote.objects.filter(person=person,).filter(
+            personal_notes = PersonalNote.objects.filter(
+                person=person,
+            ).filter(
                 Q(lesson_period__lesson__validity__school_term=school_term)
                 | Q(extra_lesson__school_term=school_term)
                 | Q(event__school_term=school_term)
diff --git a/docs/conf.py b/docs/conf.py
index 59ca213449c1f46b86ef5340aaac7c21447cb4e8..b37b0f814f16734100037dfe13bc1d8a4e007456 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -29,9 +29,9 @@ copyright = "2019-2022 The AlekSIS team"
 author = "The AlekSIS Team"
 
 # The short X.Y version
-version = "2.1"
+version = "3.0"
 # The full version, including alpha/beta/rc tags
-release = "2.2.dev0"
+release = "3.0.1.dev0"
 
 
 # -- General configuration ---------------------------------------------------
diff --git a/pyproject.toml b/pyproject.toml
index c857f98eef3eace85829b5de01f3cb7d0867f161..13197940261f1aeab62e33486e72c17718553219 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "AlekSIS-App-Alsijil"
-version = "3.0.dev0"
+version = "3.0.1.dev0"
 packages = [
     { include = "aleksis" }
 ]
@@ -29,13 +29,13 @@ maintainers = [
     "Jonathan Weth <dev@jonathanweth.de>",
 ]
 license = "EUPL-1.2"
-homepage = "https://aleksis.edugit.io/"
-repository = "https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil"
+homepage = "https://aleksis.org/"
+repository = "https://edugit.org/AlekSIS/official/AlekSIS-App-Alsijil"
 documentation = "https://aleksis.edugit.io/AlekSIS/docs/html/"
 classifiers = [
-    "Development Status :: 4 - Beta",
+    "Development Status :: 5 - Production/Stable",
     "Environment :: Web Environment",
-    "Framework :: Django :: 3.0",
+    "Framework :: Django :: 4.0",
     "Intended Audience :: Education",
     "Topic :: Education",
     "Typing :: Typed",
@@ -48,9 +48,9 @@ secondary = true
 
 [tool.poetry.dependencies]
 python = "^3.9"
-aleksis-core = "^3.0.dev3"
-aleksis-app-chronos = "^3.0.dev1"
-aleksis-app-stoelindeling = { version = "^1.0", optional = true }
+aleksis-core = "^3.0"
+aleksis-app-chronos = "^3.0"
+aleksis-app-stoelindeling = { version = "^2.0", optional = true }
 
 [tool.poetry.dev-dependencies]
 aleksis-builddeps = "*"
diff --git a/tox.ini b/tox.ini
index 78e09567e193b72a299dfb3f776499420ecc04ec..6e4b77ab1ded935257117696975c2150772cd85c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,16 +1,15 @@
 [tox]
 skipsdist = True
 skip_missing_interpreters = true
-envlist = py37,py38,py39
+envlist = py39,py310,py311
 
 [testenv]
 whitelist_externals = poetry
-		      sudo
 skip_install = true
 envdir = {toxworkdir}/globalenv
 commands_pre =
      poetry install
-     poetry run aleksis-admin webpack_bundle
+     poetry run aleksis-admin vite build
      poetry run aleksis-admin collectstatic --no-input
 commands =
     poetry run pytest --cov=. {posargs} aleksis/
@@ -27,6 +26,8 @@ commands =
     poetry run black --check --diff aleksis/
     poetry run isort -c --diff --stdout aleksis/
     poetry run flake8 {posargs} aleksis/
+    poetry run sh -c "aleksis-admin yarn run prettier --check --ignore-path={toxinidir}/.prettierignore {toxinidir}"
+    poetry run sh -c "aleksis-admin yarn run eslint {toxinidir}/aleksis/**/*/frontend/**/*.{js,vue} --config={toxinidir}/.eslintrc.js --resolve-plugins-relative-to=."
 
 [testenv:security]
 commands =
@@ -46,6 +47,7 @@ commands = poetry run make -C docs/ html {posargs}
 commands =
     poetry run isort aleksis/
     poetry run black aleksis/
+    poetry run sh -c "aleksis-admin yarn run prettier --write --ignore-path={toxinidir}/.prettierignore {toxinidir}"
 
 [testenv:makemessages]
 commands =