diff --git a/.dev-js/.eslintrc.js b/.dev-js/.eslintrc.js
new file mode 100644
index 0000000000000000000000000000000000000000..40571d02f4c429d79d9ec85a4f37ba8c78f74b4c
--- /dev/null
+++ b/.dev-js/.eslintrc.js
@@ -0,0 +1,252 @@
+module.exports = {
+  root: true,
+  overrides: [
+    {
+      files: ["*.js", "*.vue"],
+      // parser: "vue-eslint-parser",
+      //processor: "@graphql-eslint/graphql",
+      extends: [
+        "eslint:recommended",
+        "plugin:vue/strongly-recommended",
+        "plugin:@intlify/vue-i18n/recommended",
+      ],
+      rules: {
+        "no-unused-vars": "warn",
+        "vue/no-unused-vars": "off",
+        "vue/multi-word-component-names": "off",
+        "vue/attribute-hyphenation": "error",
+        "vue/v-slot-style": "error",
+        "@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]+$",
+          },
+        ],
+        "@intlify/vue-i18n/no-deprecated-tc": "off",
+        // 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",
+      },
+    },
+    {
+      files: ["*.graphql"],
+      parser: "@graphql-eslint/eslint-plugin",
+      plugins: ["@graphql-eslint"],
+      extends: "plugin:@graphql-eslint/operations-recommended",
+      parserOptions: {
+        graphQLConfig: {
+          schema: "./schema.json",
+          documents: "../aleksis/**/*/frontend/**/*.graphql",
+        },
+      },
+      rules: {
+        "@graphql-eslint/no-anonymous-operations": "error",
+        "@graphql-eslint/no-duplicate-fields": "error",
+        "@graphql-eslint/naming-convention": [
+          "error",
+          {
+            OperationDefinition: {
+              style: "camelCase",
+              forbiddenPrefixes: ["Query", "Mutation", "Subscription", "Get"],
+              forbiddenSuffixes: ["Query", "Mutation", "Subscription"],
+            },
+          },
+        ],
+      },
+    },
+  ],
+};
diff --git a/.dev-js/package.json b/.dev-js/package.json
index 99b0989e1dde9b0f71fdc229bac3aef501c06398..e3f89a123e2374a92c69575908c676a78cda095c 100644
--- a/.dev-js/package.json
+++ b/.dev-js/package.json
@@ -2,10 +2,12 @@
   "name": "aleksis-builddeps",
   "version": "1.0.0",
   "dependencies": {
-    "@intlify/eslint-plugin-vue-i18n": "^2.0.0",
+    "@graphql-eslint/eslint-plugin": "^4.3.0",
+    "@intlify/eslint-plugin-vue-i18n": "^3.0.0",
     "eslint": "^8.26.0",
     "eslint-config-prettier": "^9.0.0",
     "eslint-plugin-vue": "^9.7.0",
+    "graphql": "^16.10.0",
     "prettier": "^3.4.0",
     "stylelint": "^15.0.0",
     "stylelint-config-prettier": "^9.0.3",
diff --git a/.eslintrc.js b/.eslintrc.js
deleted file mode 100644
index 60317f4987d885fc380d731c6dd8792325342f44..0000000000000000000000000000000000000000
--- a/.eslintrc.js
+++ /dev/null
@@ -1,215 +0,0 @@
-module.exports = {
-  extends: [
-    "eslint:recommended",
-    "plugin:vue/strongly-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/.gitignore b/.gitignore
index 79b5b76de6f6445254cbf64f7e4fb228ffdf0ab8..613db10966de0e5b57a87f57c6fd586925ade0b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -76,6 +76,8 @@ docs/_build/
 .dev-js/.yarn
 .dev-js/.pnp.cjs
 .dev-js/.pnp.loader.mjs
+.dev-js/.yarnrc.yml
+.dev-js/schema.json
 
 # Lock files
 poetry.lock
@@ -92,3 +94,5 @@ htmlcov/
 # Data
 maintenance_mode_state.txt
 media/
+
+aleksis/core/static/style.css
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookPrintDialog.vue b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookPrintDialog.vue
index 976dccbebb60ffc9dff257bb496542410cc9aeb2..8a9058d6d1a8391d6b72db9bb239e66afcefa9a8 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookPrintDialog.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookPrintDialog.vue
@@ -20,9 +20,10 @@ import CancelButton from "aleksis.core/components/generic/buttons/CancelButton.v
       {{ $t("alsijil.coursebook.print.title") }}
     </template>
     <template #content>
-      {{ $t("alsijil.coursebook.print.groups") }}
       <v-autocomplete
+        v-if="!group"
         :items="availableGroups"
+        :label="$t('alsijil.coursebook.print.groups')"
         item-text="name"
         item-value="id"
         :value="value"
@@ -87,7 +88,16 @@ export default {
      */
     availableGroups: {
       type: Array,
-      required: true,
+      required: false,
+      default: () => [],
+    },
+    /**
+     * Set a group to use this dialog exclusively for
+     */
+    group: {
+      type: Object,
+      required: false,
+      default: null,
     },
     /**
      * Initially selected groups
@@ -121,6 +131,9 @@ export default {
   },
   computed: {
     selectedGroups() {
+      if (this.group) {
+        return [this.group.id];
+      }
       if (this.currentGroupSelection.length == 0) {
         return this.value.map((group) => group.id);
       } else {
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/personal_notes.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/personal_notes.graphql
index 014178c1326960f02dee24cf92f8eb5c679298f5..8676c02f6ad56ef3c1514c394820cbffe4586490 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/personal_notes.graphql
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/personal_notes.graphql
@@ -1,15 +1,3 @@
-query personalNotes($orderBy: [String], $filters: JSONString) {
-  items: personalNotes(orderBy: $orderBy, filters: $filters) {
-    id
-    note
-    extraMark {
-      id
-    }
-    canEdit
-    canDelete
-  }
-}
-
 mutation createPersonalNotes($input: [BatchCreatePersonalNoteInput]!) {
   createPersonalNotes(input: $input) {
     items: personalNotes {
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForGroupTab.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForGroupTab.vue
index 8f7d5b428da74d4bc3dc438dea0e0140678378cd..1575348b914f2afb1b86e4aca933bf991eeabd62 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForGroupTab.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForGroupTab.vue
@@ -10,6 +10,9 @@
     :show-select="false"
     @items="items = $event"
   >
+    <template #additionalActions>
+      <coursebook-print-dialog :group="group" />
+    </template>
     <template
       v-for="(extraMark, index) in extraMarks"
       #[`extraMarks.${index}.count`]="{ item }"
@@ -65,11 +68,11 @@
         i18n-key="alsijil.coursebook.statistics.person_view_details"
         icon-text="mdi-open-in-new"
         :to="{
-          name: 'alsijil.coursebook_statistics',
+          name: 'core.personById',
           params: {
-            personId: item.person.id,
-            mode: MODE.PARTICIPATIONS,
+            id: item.person.id,
           },
+          hash: '#' + MODE.PARTICIPATIONS,
         }"
       />
     </template>
@@ -81,13 +84,14 @@ import groupOverviewTabMixin from "aleksis.core/mixins/groupOverviewTabMixin.js"
 import CRUDList from "aleksis.core/components/generic/CRUDList.vue";
 import PersonChip from "aleksis.core/components/person/PersonChip.vue";
 import SecondaryActionButton from "aleksis.core/components/generic/buttons/SecondaryActionButton.vue";
+import CoursebookPrintDialog from "../CoursebookPrintDialog.vue";
 
 import AbsenceReasonChip from "aleksis.apps.kolego/components/AbsenceReasonChip.vue";
 import ExtraMarkChip from "aleksis.apps.alsijil/components/extra_marks/ExtraMarkChip.vue";
 
 import { statisticsByGroup } from "./statistics.graphql";
 import { absenceReasons } from "../queries/absenceReasons.graphql";
-import { extraMarks } from "../../extra_marks/extra_marks.graphql";
+import { extraMarks } from "../queries/extraMarks.graphql";
 import { MODE } from "./modes";
 
 export default {
@@ -99,6 +103,7 @@ export default {
     ExtraMarkChip,
     PersonChip,
     SecondaryActionButton,
+    CoursebookPrintDialog,
   },
   data() {
     return {
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonCard.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonCard.vue
index 546fc8e00701798b801427355328c14d0237578b..fd270fc374e7df35ed27fe76cef779fdf402ae47 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonCard.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonCard.vue
@@ -7,18 +7,7 @@
     <v-card-title v-else-if="compact">
       {{ $t("alsijil.coursebook.statistics.person_compact.title") }}
       <v-spacer />
-      <base-button
-        :icon="true"
-        icon-text="mdi-open-in-new"
-        i18n-key=""
-        :to="{
-          name: 'alsijil.coursebook_statistics',
-          params: {
-            personId: person.id,
-            mode: MODE.PARTICIPATIONS,
-          },
-        }"
-      />
+      <slot name="header" />
     </v-card-title>
     <v-card-title v-else>
       {{ $t("alsijil.coursebook.statistics.title_plural") }}
@@ -59,7 +48,6 @@
 
 <script>
 import personOverviewCardMixin from "aleksis.core/mixins/personOverviewCardMixin.js";
-import BaseButton from "aleksis.core/components/generic/buttons/BaseButton.vue";
 import MessageBox from "aleksis.core/components/generic/MessageBox.vue";
 import StatisticsAbsencesCard from "./StatisticsAbsencesCard.vue";
 import StatisticsTardinessCard from "./StatisticsTardinessCard.vue";
@@ -73,7 +61,6 @@ export default {
   name: "StatisticsForPersonCard",
   mixins: [personOverviewCardMixin],
   components: {
-    BaseButton,
     MessageBox,
     StatisticsAbsencesCard,
     StatisticsTardinessCard,
@@ -111,6 +98,9 @@ export default {
     MODE() {
       return MODE;
     },
+    mode() {
+      return this.$hash;
+    },
     gridTemplateAreas() {
       return this.compact
         ? `"absences extra_marks" "tardinesses tardinesses"`
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue
index d90e166d264c5fde89d87d2bcd2c5366836149d1..c9c8bcd3ce8d762c9fe091f343adda5307cbfe8b 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue
@@ -2,6 +2,13 @@
   <fullscreen-dialog-page
     :fallback-url="{ name: 'core.personById', props: { id: personId } }"
   >
+    <template #title>
+      {{
+        $t("alsijil.coursebook.statistics.person_page.title", {
+          fullName: personName?.fullName || "???",
+        })
+      }}
+    </template>
     <div class="d-flex" style="gap: 4em">
       <div class="flex-grow-1" style="max-width: 100%">
         <!-- documentations for person list -->
@@ -172,6 +179,14 @@
                         <v-list-item-action
                           class="flex-row full-width justify-md-end ma-0 align-center fill-height"
                         >
+                          <v-chip
+                            color="warning"
+                            class="mx-1"
+                            v-if="!item.relatedDocumentation.amended"
+                            >{{
+                              $t("alsijil.coursebook.statistics.not_counted")
+                            }}</v-chip
+                          >
                           <!-- chips: absences & extraMarks -->
                           <absence-reason-chip
                             v-if="item.absenceReason"
@@ -301,11 +316,6 @@ export default {
       type: [Number, String],
       required: true,
     },
-    mode: {
-      type: String,
-      required: false,
-      default: MODE.PARTICIPATIONS,
-    },
   },
   apollo: {
     personName: {
@@ -315,13 +325,6 @@ export default {
           person: this.personId,
         };
       },
-      result({ data }) {
-        this.$setToolBarTitle(
-          this.$t("alsijil.coursebook.statistics.person_page.title", {
-            fullName: data.personName.fullName || "???",
-          }),
-        );
-      },
     },
     absenceReasons: {
       query: absenceReasons,
@@ -346,6 +349,9 @@ export default {
     MODE() {
       return MODE;
     },
+    mode() {
+      return this.$hash;
+    },
   },
   methods: {
     gqlQuery() {
@@ -360,12 +366,9 @@ export default {
 
       this.selected = [];
 
-      this.$router.push({
-        name: "alsijil.coursebook_statistics",
-        params: {
-          personId: this.personId,
-          mode: mode,
-        },
+      this.$router.replace({
+        ...this.$route,
+        hash: "#" + mode,
       });
     },
     showEdit(item) {
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonWidget.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonWidget.vue
new file mode 100644
index 0000000000000000000000000000000000000000..495e08c016bbcb9cb3dfc5f4a1bd9475da1a4c6a
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonWidget.vue
@@ -0,0 +1,47 @@
+<template>
+  <statistics-for-person-card v-bind="{ ...$attrs, ...$props }">
+    <template #header>
+      <base-button
+        :icon="true"
+        icon-text="mdi-open-in-new"
+        i18n-key=""
+        :to="{
+          name: $route.name,
+          params: $route.params,
+          hash: '#' + MODE.PARTICIPATIONS,
+        }"
+      />
+      <statistics-for-person-page
+        v-if="Object.values(MODE).includes(mode)"
+        :person-id="person.id"
+      />
+    </template>
+  </statistics-for-person-card>
+</template>
+
+<script>
+import personOverviewCardMixin from "aleksis.core/mixins/personOverviewCardMixin.js";
+import BaseButton from "aleksis.core/components/generic/buttons/BaseButton.vue";
+import StatisticsForPersonPage from "./StatisticsForPersonPage.vue";
+import StatisticsForPersonCard from "./StatisticsForPersonCard.vue";
+
+import { MODE } from "./modes";
+
+export default {
+  name: "StatisticsForPersonWidget",
+  mixins: [personOverviewCardMixin],
+  components: {
+    BaseButton,
+    StatisticsForPersonPage,
+    StatisticsForPersonCard,
+  },
+  computed: {
+    MODE() {
+      return MODE;
+    },
+    mode() {
+      return this.$hash;
+    },
+  },
+};
+</script>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/modes.js b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/modes.js
index c345316a23b880f73acddf2271759de63a6e760d..54ca270a7dd598878bed03179206e7cd1edce7be 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/modes.js
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/modes.js
@@ -1,4 +1,4 @@
 export const MODE = {
-  PARTICIPATIONS: "participations",
-  PERSONAL_NOTES: "personal_notes",
+  PARTICIPATIONS: "alsijil.participations",
+  PERSONAL_NOTES: "alsijil.personal_notes",
 };
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql
index 1cffdc7178defa256f5f9e46707541a526cb1dec..a83e3fa59d96aa5cf9fcdb103c73ceb0546d31a1 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql
@@ -64,6 +64,7 @@ query participationsOfPerson($person: ID!) {
         colourFg
         colourBg
       }
+      amended
     }
     canEdit
     canDelete
diff --git a/aleksis/apps/alsijil/frontend/components/extra_marks/ExtraMarkButtons.vue b/aleksis/apps/alsijil/frontend/components/extra_marks/ExtraMarkButtons.vue
index 01deef40190efb422376dc7848185e8916400b43..8a8485bcca6e1ff9e33d62893879398f3188fab1 100644
--- a/aleksis/apps/alsijil/frontend/components/extra_marks/ExtraMarkButtons.vue
+++ b/aleksis/apps/alsijil/frontend/components/extra_marks/ExtraMarkButtons.vue
@@ -1,5 +1,5 @@
 <script>
-import { extraMarks } from "./extra_marks.graphql";
+import { extraMarksList } from "./extra_marks.graphql";
 
 export default {
   name: "ExtraMarkButtons",
@@ -10,7 +10,7 @@ export default {
   },
   apollo: {
     extraMarks: {
-      query: extraMarks,
+      query: extraMarksList,
       update: (data) => data.items,
       skip() {
         return this.customExtraMarks.length > 0;
diff --git a/aleksis/apps/alsijil/frontend/components/extra_marks/ExtraMarks.vue b/aleksis/apps/alsijil/frontend/components/extra_marks/ExtraMarks.vue
index 9468ac1dba5d291abd251659586d24aa95fe0278..4d586e852225ee9592a5634807386012b2c57405 100644
--- a/aleksis/apps/alsijil/frontend/components/extra_marks/ExtraMarks.vue
+++ b/aleksis/apps/alsijil/frontend/components/extra_marks/ExtraMarks.vue
@@ -87,7 +87,7 @@ import InlineCRUDList from "aleksis.core/components/generic/InlineCRUDList.vue";
 <script>
 import formRulesMixin from "aleksis.core/mixins/formRulesMixin.js";
 import {
-  extraMarks,
+  extraMarksList,
   createExtraMarks,
   deleteExtraMarks,
   updateExtraMarks,
@@ -121,7 +121,7 @@ export default {
         },
       ],
       i18nKey: "alsijil.extra_marks",
-      gqlQuery: extraMarks,
+      gqlQuery: extraMarksList,
       gqlCreateMutation: createExtraMarks,
       gqlPatchMutation: updateExtraMarks,
       gqlDeleteMutation: deleteExtraMarks,
diff --git a/aleksis/apps/alsijil/frontend/components/extra_marks/extra_marks.graphql b/aleksis/apps/alsijil/frontend/components/extra_marks/extra_marks.graphql
index 73e8ba4121c215dc4c3968b3ed2021b71b5ecfc1..19e763f3b83639661b375ceddca9da2d73c34ad8 100644
--- a/aleksis/apps/alsijil/frontend/components/extra_marks/extra_marks.graphql
+++ b/aleksis/apps/alsijil/frontend/components/extra_marks/extra_marks.graphql
@@ -1,4 +1,4 @@
-query extraMarks($orderBy: [String], $filters: JSONString) {
+query extraMarksList($orderBy: [String], $filters: JSONString) {
   items: extraMarks(orderBy: $orderBy, filters: $filters) {
     id
     shortName
diff --git a/aleksis/apps/alsijil/frontend/index.js b/aleksis/apps/alsijil/frontend/index.js
index fe222f4fee3fcc28ad93448b4482babcf0ab32cf..18e0f68eaca178cbaf3b358a8065257cc3e745b3 100644
--- a/aleksis/apps/alsijil/frontend/index.js
+++ b/aleksis/apps/alsijil/frontend/index.js
@@ -1,5 +1,4 @@
 import { DateTime } from "luxon";
-import { MODE } from "./components/coursebook/statistics/modes";
 
 export const collectionItems = {
   coreGroupActions: [
@@ -32,7 +31,7 @@ export const collectionItems = {
       key: "core-person-widgets",
       component: () =>
         import(
-          "./components/coursebook/statistics/StatisticsForPersonCard.vue"
+          "./components/coursebook/statistics/StatisticsForPersonWidget.vue"
         ),
       shouldDisplay: () => true,
       colProps: {
@@ -113,21 +112,5 @@ export default {
         permission: "alsijil.view_extramarks_rule",
       },
     },
-    {
-      path: `statistics/:personId/:mode(${Object.values(MODE).join("|")})`,
-      component: () =>
-        import(
-          "./components/coursebook/statistics/StatisticsForPersonPage.vue"
-        ),
-      name: "alsijil.coursebook_statistics",
-      props: true,
-      meta: {
-        inMenu: false,
-        titleKey: "alsijil.coursebook.statistics.person_compact.title",
-        toolbarTitle: "alsijil.coursebook.statistics.person_compact.title",
-        // TODO: Add permission & change it here.
-        permission: "alsijil.view_documentations_menu_rule",
-      },
-    },
   ],
 };
diff --git a/aleksis/apps/alsijil/frontend/messages/de.json b/aleksis/apps/alsijil/frontend/messages/de.json
index 4d134acfe04b8b28f42bc89c21c31ddccd5db5a0..b1650ed2b7076aef89c4405f7a87540a84a71460 100644
--- a/aleksis/apps/alsijil/frontend/messages/de.json
+++ b/aleksis/apps/alsijil/frontend/messages/de.json
@@ -82,7 +82,8 @@
           "title": "Kursbuch · Statistiken · {fullName}"
         },
         "person_view_details": "Details",
-        "title_plural": "Statistiken"
+        "title_plural": "Statistiken",
+        "not_counted": "nicht gezählt"
       },
       "status": {
         "available": "Kursbucheintrag vorhanden",
diff --git a/aleksis/apps/alsijil/frontend/messages/en.json b/aleksis/apps/alsijil/frontend/messages/en.json
index a40f8d5475a9c3e9843e4a72ac745c723a1137e3..99ce86919f439acaed9902bdaae709ddefda8cd1 100644
--- a/aleksis/apps/alsijil/frontend/messages/en.json
+++ b/aleksis/apps/alsijil/frontend/messages/en.json
@@ -85,7 +85,8 @@
           "summary": "Summary"
         },
         "person_view_details": "Details",
-        "title_plural": "Statistics"
+        "title_plural": "Statistics",
+        "not_counted": "not counted"
       },
       "notes": {
         "show_list": "List of participants",
diff --git a/aleksis/apps/alsijil/model_extensions.py b/aleksis/apps/alsijil/model_extensions.py
index a3d9c14fafedc02858b025ef221d97b08bfb5b90..e1a2bd0ef2a2c49d0bdf124b21a6f550d303176c 100644
--- a/aleksis/apps/alsijil/model_extensions.py
+++ b/aleksis/apps/alsijil/model_extensions.py
@@ -2,6 +2,7 @@ from django.db.models import FilteredRelation, Q, QuerySet, Value
 from django.db.models.aggregates import Count, Sum
 from django.utils.translation import gettext as _
 
+from aleksis.apps.chronos.models import LessonEvent
 from aleksis.apps.kolego.models import AbsenceReason
 from aleksis.core.models import Group, Person, SchoolTerm
 
@@ -137,13 +138,6 @@ def annotate_person_statistics_for_school_term(
         datetime_end__date__lte=school_term.date_end,
     )
     if group:
-        documentations = documentations.filter(
-            pk__in=Documentation.objects.filter(course__groups=group)
-            .values_list("pk", flat=True)
-            .union(
-                Documentation.objects.filter(course__groups__parent_groups=group).values_list(
-                    "pk", flat=True
-                )
-            )
-        )
+        lesson_events = LessonEvent.objects.filter(LessonEvent.objects.for_group_q(group))
+        documentations = documentations.filter(amends__in=lesson_events)
     return annotate_person_statistics_from_documentations(persons, documentations)
diff --git a/aleksis/apps/alsijil/schema/documentation.py b/aleksis/apps/alsijil/schema/documentation.py
index ad88b2de383217984eb4cb8e2e2704daf68c84e9..9548581a63066a3c9e49465bd99ded9a95678dbc 100644
--- a/aleksis/apps/alsijil/schema/documentation.py
+++ b/aleksis/apps/alsijil/schema/documentation.py
@@ -48,6 +48,7 @@ class DocumentationType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectTyp
 
     course = graphene.Field(CourseType, required=False)
     amends = graphene.Field(lambda: LessonEventType, required=False)
+    amended = graphene.Boolean(required=False)
     subject = graphene.Field(SubjectType, required=False)
     participations = graphene.List(ParticipationStatusType, required=False)
 
@@ -66,6 +67,11 @@ class DocumentationType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectTyp
             return root._amends_prefetched
         return root.amends
 
+    @staticmethod
+    @bypass_get_queryset
+    def resolve_amended(root: Documentation, info, **kwargs):
+        return root.amends_id is not None
+
     @staticmethod
     @bypass_get_queryset
     def resolve_teachers(root: Documentation, info, **kwargs):
diff --git a/aleksis/apps/alsijil/tasks.py b/aleksis/apps/alsijil/tasks.py
index 5c0377d7468a56dd6539c18c8ea80e15b69738b6..71de0d315e6a2d7fbb66b99d5298d21b52502429 100644
--- a/aleksis/apps/alsijil/tasks.py
+++ b/aleksis/apps/alsijil/tasks.py
@@ -7,6 +7,7 @@ from django.utils.translation import gettext as _
 from celery.result import allow_join_result
 from celery.states import SUCCESS
 
+from aleksis.apps.chronos.models import LessonEvent
 from aleksis.apps.cursus.models import Course
 from aleksis.apps.kolego.models.absence import AbsenceReason
 from aleksis.core.models import Group, PDFFile
@@ -118,6 +119,13 @@ def generate_full_register_printout(
                             course__groups__parent_groups=group
                         ).values_list("pk", flat=True)
                     )
+                    .union(
+                        Documentation.objects.filter(
+                            amends__in=LessonEvent.objects.filter(
+                                LessonEvent.objects.for_group_q(group)
+                            )
+                        ).values_list("pk", flat=True)
+                    )
                 )
             )
 
diff --git a/pyproject.toml b/pyproject.toml
index 213cef02d306616d8a7c911059215dbe0edcaa07..50737901a7a3578cb3960c9a46b0b0500253ceea 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -48,7 +48,7 @@ url = "https://edugit.org/api/v4/projects/461/packages/pypi/simple"
 priority = "supplemental"
 [tool.poetry.dependencies]
 python = "^3.10"
-aleksis-core = "^4.0.0.dev11"
+aleksis-core = "^4.0.0.dev16"
 aleksis-app-chronos = "^4.0.0.dev7"
 aleksis-app-kolego = "^0.1.0.dev3"
 
diff --git a/tox.ini b/tox.ini
index 92f26b55d9ebd6f5b3db425cab8d151bde0c0c69..7bccb6bf67559b0150b61d36c83f5035ff7856a7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -24,12 +24,13 @@ setenv =
 
 [testenv:lint]
 commands_pre =
-    poetry install --only=dev
+    poetry install
     yarnpkg --cwd=.dev-js
 commands =
     poetry run ruff check {posargs} aleksis/
     yarnpkg --cwd=.dev-js run prettier --ignore-path={toxinidir}/.prettierignore {posargs} --check ..
-    yarnpkg --cwd=.dev-js run eslint ../aleksis/**/*/frontend/**/*.{js,vue} --config={toxinidir}/.eslintrc.js --resolve-plugins-relative-to=.
+    poetry run aleksis-admin graphql_schema --schema aleksis.core.schema.schema --out .dev-js/schema.json
+    yarnpkg --cwd=.dev-js run eslint ../aleksis/**/*/frontend/**/*.{js,vue,graphql} --config={toxinidir}/.dev-js/.eslintrc.js
 
 [testenv:security]
 commands_pre =
@@ -42,6 +43,8 @@ commands =
 commands_pre =
     poetry install
     poetry run sh -c "cd aleksis; aleksis-admin compilemessages"
+    poetry run aleksis-admin yarn install
+    poetry run aleksis-admin compile_scss
 commands = poetry build
 
 [testenv:docs]