diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 906f44538761575dc686cb0569f9b2fb61145e94..f7b0d177ba0f73ca88d7e3c634459fa7cf0e305c 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -6,6 +6,15 @@ 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`_.
 
+`4.0.0.dev2`_ - 2024-07-10
+--------------------------
+
+Added
+~~~~~
+
+* Support for entering personal notes for students in the new coursebook interface.
+* Support for entering tardiness for students in the new coursebook interface.
+
 `4.0.0.dev1`_ - 2024-06-13
 --------------------------
 
@@ -41,6 +50,14 @@ Fixed
 
 * Migrating failed due to an incorrect field reference.
 
+`3.0.1`_ - 2023-09-02
+-------------------
+
+Fixed
+~~~~~
+
+* Migrations failed on empty database
+
 `3.0`_ - 2023-05-15
 -------------------
 
@@ -132,7 +149,7 @@ Changed
 ~~~~~~~
 
 * Use start date of current SchoolTerm as default value for PersonalNote filter in overview.
-
+Julia ist eine höhere Programmiersprache, die vor allem für numerisches und wissenschaftliches Rechnen entwickelt wurde und auch als Allzweck-Programmiersprache verwendet werden kann, bei gleichzeitiger Wahrung einer hohen Ausführungsgeschwindigkeit. Wikipedia
 Fixed
 ~~~~~
 
@@ -351,4 +368,7 @@ Fixed
 .. _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
+.. _3.0.1: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/3.0.1
 .. _4.0.0.dev0: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/4.0.0.dev0
+.. _4.0.0.dev1: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/4.0.0.dev1
+.. _4.0.0.dev2: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/4.0.0.dev2
diff --git a/README.rst b/README.rst
index 61e80f73c24aa9077218aa86a2427463f75f1f0f..53fbc493f0f1acfd35a0f893c816cfb117a27572 100644
--- a/README.rst
+++ b/README.rst
@@ -34,10 +34,11 @@ Licence
   Copyright © 2019, 2021 Dominik George <dominik.george@teckids.org>
   Copyright © 2019, 2020 Tom Teichler <tom.teichler@teckids.org>
   Copyright © 2019 mirabilos <thorsten.glaser@teckids.org>
-  Copyright © 2020, 2021, 2022 Jonathan Weth <dev@jonathanweth.de>
-  Copyright © 2020, 2021 Julian Leucker <leuckeju@katharineum.de>
-  Copyright © 2020, 2022 Hangzhi Yu <yuha@katharineum.de>
+  Copyright © 2020, 2021, 2022, 2024 Jonathan Weth <dev@jonathanweth.de>
+  Copyright © 2020, 2021, 2024 Julian Leucker <leuckeju@katharineum.de>
+  Copyright © 2020, 2022, 2023, 2024 Hangzhi Yu <yuha@katharineum.de>
   Copyright © 2021 Lloyd Meins <meinsll@katharineum.de>
+  Copyright © 2024 Michael Bauer <michael-bauer@posteo.de>
 
 
   Licenced under the EUPL, version 1.2 or later, by Teckids e.V. (Bonn, Germany).
diff --git a/aleksis/apps/alsijil/apps.py b/aleksis/apps/alsijil/apps.py
index b523b38afa08c965628afbfbe61327e8b03f7067..ab0877f2f457658463c034e09139d8098df5b8d3 100644
--- a/aleksis/apps/alsijil/apps.py
+++ b/aleksis/apps/alsijil/apps.py
@@ -13,8 +13,9 @@ class AlsijilConfig(AppConfig):
         ([2019, 2021], "Dominik George", "dominik.george@teckids.org"),
         ([2019, 2020], "Tom Teichler", "tom.teichler@teckids.org"),
         ([2019], "mirabilos", "thorsten.glaser@teckids.org"),
-        ([2020, 2021, 2022], "Jonathan Weth", "dev@jonathanweth.de"),
-        ([2020, 2021], "Julian Leucker", "leuckeju@katharineum.de"),
-        ([2020, 2022], "Hangzhi Yu", "yuha@katharineum.de"),
+        ([2020, 2021, 2022, 2024], "Jonathan Weth", "dev@jonathanweth.de"),
+        ([2020, 2021, 2024], "Julian Leucker", "leuckeju@katharineum.de"),
+        ([2020, 2022, 2023, 2024], "Hangzhi Yu", "yuha@katharineum.de"),
         ([2021], "Lloyd Meins", "meinsll@katharineum.de"),
+        ([2024], "Michael Bauer", "michael-bauer@posteo.de"),
     )
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue b/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue
index 11a0c85c853d0d7a175017d88c3bf8a21df936a8..7c1b9ac900da53ff7ab97a5b459e9be70891521f 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue
@@ -35,6 +35,7 @@
           @init="transition"
           :key="'day-' + date"
           ref="days"
+          :extra-marks="extraMarks"
         />
         <coursebook-loader />
 
@@ -78,6 +79,7 @@ import { documentationsForCoursebook } from "./coursebook.graphql";
 import CoursebookFilters from "./CoursebookFilters.vue";
 import CoursebookLoader from "./CoursebookLoader.vue";
 import CoursebookEmptyMessage from "./CoursebookEmptyMessage.vue";
+import { extraMarks } from "../extra_marks/extra_marks.graphql";
 
 import AbsenceCreationDialog from "./absences/AbsenceCreationDialog.vue";
 
@@ -140,8 +142,15 @@ export default {
       initDate: false,
       currentDate: "",
       hashUpdater: false,
+      extraMarks: [],
     };
   },
+  apollo: {
+    extraMarks: {
+      query: extraMarks,
+      update: (data) => data.items,
+    },
+  },
   computed: {
     // Assertion: Should only fire on page load or selection change.
     //            Resets date range.
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookDay.vue b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookDay.vue
index 2f15c3fd031c62ff9fc96cf5c35042f2274d3321..c4a677c9f5f72227537f7d842baa51902d9859ac 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookDay.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookDay.vue
@@ -12,6 +12,7 @@
         >
           <documentation-modal
             :documentation="doc"
+            :extra-marks="extraMarks"
             :affected-query="lastQuery"
           />
         </v-list-item>
@@ -45,6 +46,10 @@ export default {
       required: false,
       default: false,
     },
+    extraMarks: {
+      type: Array,
+      required: true,
+    },
   },
   emits: ["init"],
   methods: {
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookFilters.vue b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookFilters.vue
index b47ebbfbb1d1cd8219686f05085eb0eeb8f453d3..00ff6b02fe2420d21396c500d20eca9a0f4678b1 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookFilters.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookFilters.vue
@@ -3,6 +3,7 @@
     <v-autocomplete
       :items="selectable"
       item-text="name"
+      :item-value="(item) => `${item.__typename}-${item.id}`"
       clearable
       return-object
       filled
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue
index 3af1db58846f37b5e7e7837dba08a4468294269e..c0207c87ae5f44a5e36549474bfe02f146202e63 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue
@@ -9,18 +9,26 @@ import documentationPartMixin from "../documentation/documentationPartMixin";
 import LessonInformation from "../documentation/LessonInformation.vue";
 import { updateParticipationStatuses } from "./participationStatus.graphql";
 import SlideIterator from "aleksis.core/components/generic/SlideIterator.vue";
+import PersonalNotes from "../personal_notes/PersonalNotes.vue";
+import ExtraMarkChip from "../../extra_marks/ExtraMarkChip.vue";
+import TardinessChip from "./TardinessChip.vue";
+import TardinessField from "./TardinessField.vue";
 
 export default {
   name: "ManageStudentsDialog",
   extends: MobileFullscreenDialog,
   components: {
+    TardinessChip,
+    ExtraMarkChip,
     AbsenceReasonChip,
     AbsenceReasonGroupSelect,
     AbsenceReasonButtons,
+    PersonalNotes,
     CancelButton,
     LessonInformation,
     MobileFullscreenDialog,
     SlideIterator,
+    TardinessField,
   },
   mixins: [documentationPartMixin, mutateMixin],
   data() {
@@ -46,14 +54,27 @@ export default {
   },
   methods: {
     sendToServer(participations, field, value) {
-      if (field !== "absenceReason") return;
+      let fieldValue;
+
+      if (field === "absenceReason") {
+        fieldValue = {
+          absenceReason: value === "present" ? null : value,
+        };
+      } else if (field === "tardiness") {
+        fieldValue = {
+          tardiness: value,
+        };
+      } else {
+        console.error(`Wrong field '${field}' for sendToServer`);
+        return;
+      }
 
       this.mutate(
         updateParticipationStatuses,
         {
           input: participations.map((participation) => ({
             id: participation.id,
-            absenceReason: value === "present" ? null : value,
+            ...fieldValue,
           })),
         },
         (storedDocumentations, incomingStatuses) => {
@@ -66,6 +87,7 @@ export default {
               (part) => part.id === newStatus.id,
             );
             participationStatus.absenceReason = newStatus.absenceReason;
+            participationStatus.tardiness = newStatus.tardiness;
             participationStatus.isOptimistic = newStatus.isOptimistic;
           });
 
@@ -144,8 +166,43 @@ export default {
           <v-list-item-title>
             {{ item.person.fullName }}
           </v-list-item-title>
-          <v-list-item-subtitle v-if="item.absenceReason">
-            <absence-reason-chip small :absence-reason="item.absenceReason" />
+          <v-list-item-subtitle
+            v-if="
+              item.absenceReason ||
+              item.notesWithNote?.length > 0 ||
+              item.notesWithExtraMark?.length > 0 ||
+              item.tardiness
+            "
+            class="d-flex flex-wrap gap"
+          >
+            <absence-reason-chip
+              v-if="item.absenceReason"
+              small
+              :absence-reason="item.absenceReason"
+            />
+            <v-chip
+              v-for="note in item.notesWithNote"
+              :key="'text-note-note-overview-' + note.id"
+              small
+            >
+              <v-avatar left>
+                <v-icon small>mdi-note-outline</v-icon>
+              </v-avatar>
+              <span class="text-truncate" style="max-width: 30ch">
+                {{ note.note }}
+              </span>
+            </v-chip>
+            <extra-mark-chip
+              v-for="note in item.notesWithExtraMark"
+              :key="'extra-mark-note-overview-' + note.id"
+              :extra-mark="extraMarks.find((e) => e.id === note.extraMark.id)"
+              small
+            />
+            <tardiness-chip
+              v-if="item.tardiness"
+              :tardiness="item.tardiness"
+              small
+            />
           </v-list-item-subtitle>
         </template>
 
@@ -169,6 +226,23 @@ export default {
               :value="item.absenceReason?.id || 'present'"
               @input="sendToServer([item], 'absenceReason', $event)"
             />
+            <tardiness-field
+              v-bind="documentationPartProps"
+              :loading="loading"
+              :disabled="loading"
+              :participation="item"
+              :value="item.tardiness"
+              @input="sendToServer([item], 'tardiness', $event)"
+            />
+          </v-card-text>
+          <v-divider />
+          <v-card-text>
+            <personal-notes
+              v-bind="documentationPartProps"
+              :participation="
+                documentation.participations.find((p) => p.id === item.id)
+              "
+            />
           </v-card-text>
         </template>
       </slide-iterator>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsTrigger.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsTrigger.vue
index 572036c67955b3365bb46eb69f6ab41ee86cf074..a7854c9ae41c9fe73a7d011f60f1abd09ad42145 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsTrigger.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsTrigger.vue
@@ -58,6 +58,7 @@ export default {
     v-bind="documentationPartProps"
     @update="() => null"
     :loading-indicator="loading"
+    v-if="!documentation.amends?.cancelled"
   >
     <template #activator="{ attrs, on }">
       <v-chip
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/TardinessChip.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/TardinessChip.vue
new file mode 100644
index 0000000000000000000000000000000000000000..6be1fea3942fed64d37d2a68dde699344e55c35b
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/TardinessChip.vue
@@ -0,0 +1,34 @@
+<script>
+export default {
+  name: "TardinessChip",
+  props: {
+    tardiness: {
+      type: Number,
+      required: false,
+      default: 0,
+    },
+    loading: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+  },
+  extends: "v-chip",
+};
+</script>
+
+<template>
+  <v-chip dense outlined v-bind="$attrs" v-on="$listeners">
+    <v-avatar left>
+      <v-icon small>mdi-clock-alert-outline</v-icon>
+    </v-avatar>
+    <slot name="prepend" />
+    <slot>
+      {{ $tc("alsijil.personal_notes.minutes_late", tardiness) }}
+    </slot>
+    <slot name="append" />
+    <v-avatar right v-if="loading">
+      <v-progress-circular indeterminate :size="16" :width="2" />
+    </v-avatar>
+  </v-chip>
+</template>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/TardinessField.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/TardinessField.vue
new file mode 100644
index 0000000000000000000000000000000000000000..74a85ae09018fcc810950e6b10a52a8e5a948607
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/TardinessField.vue
@@ -0,0 +1,109 @@
+<script>
+import { DateTime } from "luxon";
+import documentationPartMixin from "../documentation/documentationPartMixin";
+import ConfirmDialog from "aleksis.core/components/generic/dialogs/ConfirmDialog.vue";
+import PositiveSmallIntegerField from "aleksis.core/components/generic/forms/PositiveSmallIntegerField.vue";
+
+export default {
+  name: "TardinessField",
+  components: { ConfirmDialog, PositiveSmallIntegerField },
+  mixins: [documentationPartMixin],
+  props: {
+    value: {
+      type: Number,
+      default: null,
+      required: false,
+    },
+    participation: {
+      type: Object,
+      required: true,
+    },
+  },
+  computed: {
+    lessonLength() {
+      const lessonStart = DateTime.fromISO(this.documentation.datetimeStart);
+      const lessonEnd = DateTime.fromISO(this.documentation.datetimeEnd);
+
+      let diff = lessonEnd.diff(lessonStart, "minutes");
+      return diff.toObject().minutes;
+    },
+  },
+  methods: {
+    lessonLengthRule(time) {
+      return (
+        time == null ||
+        time <= this.lessonLength ||
+        this.$t("alsijil.personal_notes.lesson_length_exceeded")
+      );
+    },
+    saveValue(value) {
+      this.$emit("input", value);
+      this.previousValue = value;
+    },
+    confirm() {
+      this.saveValue(0);
+    },
+    cancel() {
+      this.saveValue(this.previousValue);
+    },
+    set(newValue) {
+      if (!newValue) {
+        // this is a DELETE action, show the dialog, ...
+        this.showDeleteConfirm = true;
+        return;
+      }
+
+      this.saveValue(newValue);
+    },
+  },
+  data() {
+    return {
+      showDeleteConfirm: false,
+      previousValue: 0,
+    };
+  },
+  mounted() {
+    this.previousValue = this.value;
+  },
+};
+</script>
+
+<template>
+  <positive-small-integer-field
+    outlined
+    class="mt-1"
+    prepend-inner-icon="mdi-clock-alert-outline"
+    :suffix="$t('time.minutes')"
+    :label="$t('alsijil.personal_notes.tardiness')"
+    :rules="[lessonLengthRule]"
+    :value="value"
+    @change="set($event)"
+    v-bind="$attrs"
+  >
+    <template #append>
+      <confirm-dialog
+        v-model="showDeleteConfirm"
+        @confirm="confirm"
+        @cancel="cancel"
+      >
+        <template #title>
+          {{ $t("alsijil.personal_notes.confirm_delete") }}
+        </template>
+        <template #text>
+          {{
+            $t("alsijil.personal_notes.confirm_delete_tardiness", {
+              tardiness: previousValue,
+              name: participation.person.fullName,
+            })
+          }}
+        </template>
+      </confirm-dialog>
+    </template>
+  </positive-small-integer-field>
+</template>
+
+<style scoped>
+.mt-n1-5 {
+  margin-top: -6px;
+}
+</style>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql
index 81a3a5fb1eb3a99bef25eb9938cd254b9068981b..dd50495972c020550dec310081f0c8c149473d9e 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql
@@ -14,6 +14,7 @@ mutation updateParticipationStatuses(
         shortName
         colour
       }
+      tardiness
     }
   }
 }
@@ -35,6 +36,18 @@ mutation touchDocumentation($documentationId: ID!) {
           shortName
           colour
         }
+        notesWithExtraMark {
+          id
+          extraMark {
+            id
+            showInCoursebook
+          }
+        }
+        notesWithNote {
+          id
+          note
+        }
+        tardiness
         isOptimistic
       }
     }
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/coursebook.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/coursebook.graphql
index 6348a24f189033fc60e97325c0c69cde5d11fbc9..73f0dc3c281a5e03cfbcd7d29dff9f8dc2e61d2b 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/coursebook.graphql
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/coursebook.graphql
@@ -35,8 +35,10 @@ query documentationsForCoursebook(
     }
     amends {
       id
+      title
       amends {
         id
+        title
         teachers {
           id
           shortName
@@ -79,6 +81,18 @@ query documentationsForCoursebook(
         shortName
         colour
       }
+      notesWithExtraMark {
+        id
+        extraMark {
+          id
+          showInCoursebook
+        }
+      }
+      notesWithNote {
+        id
+        note
+      }
+      tardiness
       isOptimistic
     }
     topic
@@ -116,6 +130,7 @@ mutation createOrUpdateDocumentations($input: [DocumentationInputType]!) {
           shortName
           colour
         }
+        tardiness
         isOptimistic
       }
     }
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/documentation/DocumentationModal.vue b/aleksis/apps/alsijil/frontend/components/coursebook/documentation/DocumentationModal.vue
index 460f39f97fc15b61d0476dd99ed33d29cf43029c..630085bd593f145e5a8a15c50d686a2e48cf6a20 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/documentation/DocumentationModal.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/documentation/DocumentationModal.vue
@@ -4,7 +4,12 @@
   <mobile-fullscreen-dialog v-model="popup" max-width="500px">
     <template #activator="activator">
       <!-- list view -> activate dialog -->
-      <documentation compact v-bind="$attrs" :dialog-activator="activator" />
+      <documentation
+        compact
+        v-bind="$attrs"
+        :dialog-activator="activator"
+        :extra-marks="extraMarks"
+      />
     </template>
     <!-- dialog view -> deactivate dialog -->
     <!-- cancel | save (through lesson-summary) -->
@@ -27,5 +32,11 @@ export default {
       popup: false,
     };
   },
+  props: {
+    extraMarks: {
+      type: Array,
+      required: true,
+    },
+  },
 };
 </script>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/documentation/LessonInformation.vue b/aleksis/apps/alsijil/frontend/components/coursebook/documentation/LessonInformation.vue
index 652609dccaf430d3a4ab138f80ee2f810b84b4af..890e557e162c64868de3a0362b248cea4c2604d8 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/documentation/LessonInformation.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/documentation/LessonInformation.vue
@@ -24,7 +24,11 @@ import PersonChip from "aleksis.core/components/person/PersonChip.vue";
         'font-weight-medium': largeGrid,
       }"
     >
-      {{ documentation.course?.name }}
+      {{
+        documentation.course?.name ||
+        documentation.amends.title ||
+        documentation.amends.amends.title
+      }}
     </span>
     <div
       :class="{
@@ -38,6 +42,10 @@ import PersonChip from "aleksis.core/components/person/PersonChip.vue";
         :subject="documentation.subject"
         v-bind="compact ? dialogActivator.attrs : {}"
         v-on="compact ? dialogActivator.on : {}"
+        :class="{
+          'text-decoration-line-through': documentation.amends?.cancelled,
+        }"
+        :disabled="documentation.amends?.cancelled"
       />
       <subject-chip
         v-if="
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/documentation/LessonNotes.vue b/aleksis/apps/alsijil/frontend/components/coursebook/documentation/LessonNotes.vue
index bc0da4a742917e0639a0c1983186fad29764babb..64bbb59cb8a4bffdabf9b0d641f2a6e415a2ba69 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/documentation/LessonNotes.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/documentation/LessonNotes.vue
@@ -1,5 +1,7 @@
 <script setup>
 import AbsenceReasonChip from "aleksis.apps.kolego/components/AbsenceReasonChip.vue";
+import ExtraMarkChip from "../../extra_marks/ExtraMarkChip.vue";
+import TardinessChip from "../absences/TardinessChip.vue";
 </script>
 
 <template>
@@ -35,6 +37,56 @@ import AbsenceReasonChip from "aleksis.apps.kolego/components/AbsenceReasonChip.
       </template>
     </absence-reason-chip>
 
+    <extra-mark-chip
+      v-for="[markId, [mark, ...participations]] in Object.entries(
+        extraMarkChips,
+      )"
+      :key="'extra-mark-' + markId"
+      :extra-mark="mark"
+      dense
+    >
+      <template #append>
+        <span
+          >:
+          <span>
+            {{
+              participations
+                .slice(0, 5)
+                .map((participation) => participation.person.firstName)
+                .join(", ")
+            }}
+          </span>
+          <span v-if="participations.length > 5">
+            <!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
+            +{{ participations.length - 5 }}
+            <!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
+          </span>
+        </span>
+      </template>
+    </extra-mark-chip>
+
+    <tardiness-chip v-if="tardyParticipations.length > 0">
+      {{ $t("alsijil.personal_notes.late") }}
+
+      <template #append>
+        <span
+          >:
+          {{
+            tardyParticipations
+              .slice(0, 5)
+              .map((participation) => participation.person.firstName)
+              .join(", ")
+          }}
+
+          <span v-if="tardyParticipations.length > 5">
+            <!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
+            +{{ tardyParticipations.length - 5 }}
+            <!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
+          </span>
+        </span>
+      </template>
+    </tardiness-chip>
+
     <manage-students-trigger v-bind="documentationPartProps" />
   </div>
 </template>
@@ -51,13 +103,18 @@ export default {
     total() {
       return this.documentation.participations.length;
     },
+    /**
+     * Return the number of present people.
+     */
     present() {
       return this.documentation.participations.filter(
         (p) => p.absenceReason === null,
       ).length;
     },
+    /**
+     * Get all course attendants who have an absence reason, grouped by that reason.
+     */
     absences() {
-      // Get all course attendants who have an absence reason
       return Object.groupBy(
         this.documentation.participations.filter(
           (p) => p.absenceReason !== null,
@@ -65,6 +122,42 @@ export default {
         ({ absenceReason }) => absenceReason.id,
       );
     },
+    /**
+     * Parse and combine all extraMark notes.
+     *
+     * Notes with extraMarks are grouped by ExtraMark. ExtraMarks with the showInCoursebook property set to false are ignored.
+     * @return An object where the keys are extraMark IDs and the values have the structure [extraMark, note1, note2, ..., noteN]
+     */
+    extraMarkChips() {
+      // Apply the inner function to each participation, with value being the resulting object
+      return this.documentation.participations.reduce((value, p) => {
+        // Go through every extra mark of this participation
+        for (const { extraMark } of p.notesWithExtraMark) {
+          // Only proceed if the extraMark should be displayed here
+          if (!extraMark.showInCoursebook) {
+            continue;
+          }
+
+          // value[extraMark.id] is an Array with the structure [extraMark, note1, note2, ..., noteN]
+          if (value[extraMark.id]) {
+            value[extraMark.id].push(p);
+          } else {
+            value[extraMark.id] = [
+              this.extraMarks.find((e) => e.id === extraMark.id),
+              p,
+            ];
+          }
+        }
+
+        return value;
+      }, {});
+    },
+    /**
+     * Return a list Participations with a set tardiness
+     */
+    tardyParticipations() {
+      return this.documentation.participations.filter((p) => p.tardiness);
+    },
   },
 };
 </script>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/documentation/documentationPartMixin.js b/aleksis/apps/alsijil/frontend/components/coursebook/documentation/documentationPartMixin.js
index 88a8e852f8cc6e333303034fb5f590d174708886..35243ee3ca5e055c0d2fd5375fba614bbbd6e246 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/documentation/documentationPartMixin.js
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/documentation/documentationPartMixin.js
@@ -33,6 +33,13 @@ export default {
       required: false,
       default: () => ({ attrs: {}, on: {} }),
     },
+    /**
+     * Once loaded list of all extra marks to avoid excessive network and database queries
+     */
+    extraMarks: {
+      type: Array,
+      required: true,
+    },
   },
 
   computed: {
@@ -46,6 +53,7 @@ export default {
         compact: this.compact,
         dialogActivator: this.dialogActivator,
         affectedQuery: this.affectedQuery,
+        extraMarks: this.extraMarks,
       };
     },
   },
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/ExtraMarkNoteCheckbox.vue b/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/ExtraMarkNoteCheckbox.vue
new file mode 100644
index 0000000000000000000000000000000000000000..1f31f9ee1cf6c557ee2fe7b86134793629f9063f
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/ExtraMarkNoteCheckbox.vue
@@ -0,0 +1,104 @@
+<script>
+import {
+  createPersonalNotes,
+  deletePersonalNotes,
+} from "./personal_notes.graphql";
+import personalNoteRelatedMixin from "./personalNoteRelatedMixin";
+import mutateMixin from "aleksis.core/mixins/mutateMixin.js";
+
+export default {
+  name: "ExtraMarkNoteCheckbox",
+  mixins: [mutateMixin, personalNoteRelatedMixin],
+  props: {
+    personalNote: {
+      type: Object,
+      default: null,
+    },
+    /**
+     * Extra Mark
+     */
+    value: {
+      type: Object,
+      required: true,
+    },
+  },
+  computed: {
+    model: {
+      get() {
+        return !!this.personalNote?.id;
+      },
+      set(newValue) {
+        if (newValue && !this.personalNote) {
+          // CREATE new personal note
+          this.mutate(
+            createPersonalNotes,
+            {
+              input: [
+                {
+                  documentation: this.documentation.id,
+                  person: this.participation.person.id,
+                  extraMark: this.value.id,
+                },
+              ],
+            },
+            (storedDocumentations, incomingPersonalNotes) => {
+              const note = incomingPersonalNotes[0];
+              const documentation = storedDocumentations.find(
+                (doc) => doc.id === this.documentation.id,
+              );
+              const participationStatus = documentation.participations.find(
+                (part) => part.id === this.participation.id,
+              );
+              participationStatus.notesWithExtraMark.push(note);
+
+              return storedDocumentations;
+            },
+          );
+        } else {
+          // DELETE personal note
+          this.mutate(
+            deletePersonalNotes,
+            {
+              ids: [this.personalNote.id],
+            },
+            (storedDocumentations) => {
+              const documentation = storedDocumentations.find(
+                (doc) => doc.id === this.documentation.id,
+              );
+              const participationStatus = documentation.participations.find(
+                (part) => part.id === this.participation.id,
+              );
+              const index = participationStatus.notesWithExtraMark.findIndex(
+                (n) => n.id === this.personalNote.id,
+              );
+              participationStatus.notesWithExtraMark.splice(index, 1);
+
+              return storedDocumentations;
+            },
+          );
+        }
+      },
+    },
+  },
+};
+</script>
+
+<template>
+  <v-checkbox
+    :label="value.name"
+    :value="value.id"
+    v-model="model"
+    :disabled="loading"
+    :true-value="true"
+    :false-value="false"
+  >
+    <template #append>
+      <v-progress-circular
+        v-if="loading"
+        indeterminate
+        :size="16"
+        :width="2"
+      ></v-progress-circular>
+    </template>
+  </v-checkbox>
+</template>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/ExtraMarksNote.vue b/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/ExtraMarksNote.vue
new file mode 100644
index 0000000000000000000000000000000000000000..177b21e803f073e7965815662a00727b8b8a24e4
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/ExtraMarksNote.vue
@@ -0,0 +1,29 @@
+<script>
+import { extraMarks } from "../../extra_marks/extra_marks.graphql";
+import ExtraMarkNoteCheckbox from "./ExtraMarkNoteCheckbox.vue";
+import personalNoteRelatedMixin from "./personalNoteRelatedMixin";
+
+export default {
+  name: "ExtraMarksNote",
+  components: { ExtraMarkNoteCheckbox },
+  mixins: [personalNoteRelatedMixin],
+  props: {
+    value: {
+      type: Array,
+      required: true,
+    },
+  },
+};
+</script>
+
+<template>
+  <div>
+    <extra-mark-note-checkbox
+      v-for="extraMark in extraMarks"
+      :key="'checkbox-extramark-' + extraMark.id"
+      v-bind="personalNoteRelatedProps"
+      :value="extraMark"
+      :personal-note="value.find((pn) => pn.extraMark.id === extraMark.id)"
+    />
+  </div>
+</template>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/PersonalNotes.vue b/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/PersonalNotes.vue
new file mode 100644
index 0000000000000000000000000000000000000000..2a3d953f03484ef2af2f1ac7a8f876bcb658f8bf
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/PersonalNotes.vue
@@ -0,0 +1,28 @@
+<script setup>
+import ExtraMarksNote from "./ExtraMarksNote.vue";
+import TextNotes from "./TextNotes.vue";
+</script>
+<script>
+import personalNoteRelatedMixin from "./personalNoteRelatedMixin";
+
+export default {
+  name: "PersonalNotes",
+  mixins: [personalNoteRelatedMixin],
+};
+</script>
+
+<template>
+  <div>
+    <text-notes
+      v-bind="personalNoteRelatedProps"
+      :value="participation.notesWithNote"
+    />
+
+    <extra-marks-note
+      v-bind="personalNoteRelatedProps"
+      :value="participation.notesWithExtraMark"
+    />
+  </div>
+</template>
+
+<style scoped></style>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/TextNote.vue b/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/TextNote.vue
new file mode 100644
index 0000000000000000000000000000000000000000..43175c7402100c5ef9e4b84c153a8e79a9bb7f76
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/TextNote.vue
@@ -0,0 +1,164 @@
+<script>
+import {
+  createPersonalNotes,
+  deletePersonalNotes,
+  updatePersonalNotes,
+} from "./personal_notes.graphql";
+import personalNoteRelatedMixin from "./personalNoteRelatedMixin";
+import mutateMixin from "aleksis.core/mixins/mutateMixin.js";
+import DeleteDialog from "aleksis.core/components/generic/dialogs/DeleteDialog.vue";
+
+export default {
+  name: "TextNote",
+  components: { DeleteDialog },
+  mixins: [mutateMixin, personalNoteRelatedMixin],
+  props: {
+    value: {
+      type: Object,
+      required: true,
+    },
+  },
+  computed: {
+    model: {
+      get() {
+        return this.value.note;
+      },
+      set(newValue) {
+        if (!newValue) {
+          // this is a DELETE action, show the dialog, ...
+          this.showDeleteConfirm = true;
+          return;
+        }
+        const create = !this.value.id;
+
+        this.mutate(
+          create ? createPersonalNotes : updatePersonalNotes,
+          this.getInput(
+            newValue,
+            create
+              ? {
+                  documentation: this.documentation.id,
+                  person: this.participation.person.id,
+                  extraMark: null,
+                }
+              : {
+                  id: this.value.id,
+                },
+          ),
+          this.getUpdater(create ? "create" : "update"),
+        );
+      },
+    },
+  },
+  methods: {
+    getInput(newValue, extras) {
+      return {
+        input: [
+          {
+            note: newValue,
+            ...extras,
+          },
+        ],
+      };
+    },
+    getUpdater(mode) {
+      return (storedDocumentations, incomingPersonalNotes) => {
+        const note = incomingPersonalNotes?.[0] || undefined;
+        const documentation = storedDocumentations.find(
+          (doc) => doc.id === this.documentation.id,
+        );
+        const participationStatus = documentation.participations.find(
+          (part) => part.id === this.participation.id,
+        );
+        switch (mode.toLowerCase()) {
+          case "update":
+          case "delete": {
+            const updateIndex = participationStatus.notesWithNote.findIndex(
+              (n) => n.id === this.value.id,
+            );
+            if (mode === "update") {
+              participationStatus.notesWithNote.splice(updateIndex, 1, note);
+            } else {
+              participationStatus.notesWithNote.splice(updateIndex, 1);
+            }
+
+            break;
+          }
+
+          case "create":
+            participationStatus.notesWithNote.push(note);
+
+            this.$emit("create");
+            break;
+
+          default:
+            console.error("Invalid mode in getUpdater():", mode);
+            break;
+        }
+
+        return storedDocumentations;
+      };
+    },
+  },
+  data() {
+    return {
+      showDeleteConfirm: false,
+      deletePersonalNotes,
+    };
+  },
+};
+</script>
+
+<template>
+  <v-textarea
+    auto-grow
+    :rows="3"
+    outlined
+    hide-details
+    class="mb-4"
+    :label="$t('alsijil.personal_notes.note')"
+    :value="model"
+    @change="model = $event"
+    :loading="loading"
+  >
+    <template #append>
+      <v-slide-x-reverse-transition>
+        <v-btn
+          v-if="!!model"
+          icon
+          @click="showDeleteConfirm = true"
+          class="mt-n1-5"
+        >
+          <v-icon> $deleteContent </v-icon>
+        </v-btn>
+      </v-slide-x-reverse-transition>
+
+      <delete-dialog
+        v-model="showDeleteConfirm"
+        :gql-delete-mutation="deletePersonalNotes"
+        :affected-query="affectedQuery"
+        item-attribute="fullName"
+        :items="[value]"
+        :custom-update="getUpdater('delete')"
+      >
+        <template #title>
+          {{ $t("alsijil.personal_notes.confirm_delete") }}
+        </template>
+        <template #body>
+          {{
+            $t("alsijil.personal_notes.confirm_delete_explanation", {
+              note: value.note,
+              name: participation.person.fullName,
+            })
+          }}
+        </template>
+      </delete-dialog>
+    </template>
+  </v-textarea>
+</template>
+
+<style scoped>
+.mt-n1-5 {
+  margin-top: -6px;
+}
+</style>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/TextNotes.vue b/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/TextNotes.vue
new file mode 100644
index 0000000000000000000000000000000000000000..dd32164db251b16acdc8b61daa29559b9a776fde
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/TextNotes.vue
@@ -0,0 +1,48 @@
+<script setup>
+import SecondaryActionButton from "aleksis.core/components/generic/buttons/SecondaryActionButton.vue";
+import TextNote from "./TextNote.vue";
+</script>
+<script>
+import personalNoteRelatedMixin from "./personalNoteRelatedMixin";
+
+export default {
+  name: "TextNotes",
+  mixins: [personalNoteRelatedMixin],
+  props: {
+    value: {
+      type: Array,
+      required: true,
+    },
+  },
+  data() {
+    return {
+      showNewNote: true,
+    };
+  },
+  computed: {
+    notes() {
+      return this.showNewNote ? [...this.value, { note: "" }] : this.value;
+    },
+  },
+};
+</script>
+
+<template>
+  <div>
+    <text-note
+      v-for="note in notes"
+      :key="note.id || -1"
+      v-bind="personalNoteRelatedProps"
+      :value="note"
+      @create="showNewNote = false"
+    />
+
+    <secondary-action-button
+      i18n-key="alsijil.personal_notes.create_personal_note"
+      icon-text="$plus"
+      class="full-width"
+      @click="showNewNote = true"
+      :disabled="showNewNote"
+    />
+  </div>
+</template>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/personalNoteRelatedMixin.js b/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/personalNoteRelatedMixin.js
new file mode 100644
index 0000000000000000000000000000000000000000..0eda6c4ca625afb16742d9ea8e585254d041e970
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/personalNoteRelatedMixin.js
@@ -0,0 +1,19 @@
+import documentationPartMixin from "../documentation/documentationPartMixin";
+
+export default {
+  mixins: [documentationPartMixin],
+  props: {
+    participation: {
+      type: Object,
+      required: true,
+    },
+  },
+  computed: {
+    personalNoteRelatedProps() {
+      return {
+        ...this.documentationPartProps,
+        participation: this.participation,
+      };
+    },
+  },
+};
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
new file mode 100644
index 0000000000000000000000000000000000000000..014178c1326960f02dee24cf92f8eb5c679298f5
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/personal_notes/personal_notes.graphql
@@ -0,0 +1,45 @@
+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 {
+      id
+      note
+      extraMark {
+        id
+      }
+      canEdit
+      canDelete
+    }
+  }
+}
+
+mutation deletePersonalNotes($ids: [ID]!) {
+  deletePersonalNotes(ids: $ids) {
+    deletionCount
+  }
+}
+
+mutation updatePersonalNotes($input: [BatchPatchPersonalNoteInput]!) {
+  updatePersonalNotes(input: $input) {
+    items: personalNotes {
+      id
+      note
+      extraMark {
+        id
+      }
+      canEdit
+      canDelete
+    }
+  }
+}
diff --git a/aleksis/apps/alsijil/frontend/components/extra_marks/ExtraMarkChip.vue b/aleksis/apps/alsijil/frontend/components/extra_marks/ExtraMarkChip.vue
new file mode 100644
index 0000000000000000000000000000000000000000..da5cd4182d6f25965374934a31d64691dbdf852f
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/extra_marks/ExtraMarkChip.vue
@@ -0,0 +1,49 @@
+<script>
+import CounterChip from "aleksis.core/components/generic/chips/CounterChip.vue";
+
+export default {
+  name: "ExtraMarkChip",
+  components: { CounterChip },
+  props: {
+    extraMark: {
+      type: Object,
+      required: true,
+    },
+    short: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+    loading: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+  },
+  extends: CounterChip,
+  computed: {
+    text() {
+      return this.short ? this.extraMark.shortName : this.extraMark.name;
+    },
+  },
+};
+</script>
+
+<template>
+  <counter-chip
+    :color="extraMark.colourBg"
+    :text-color="extraMark.colourFg"
+    :value="extraMark.id"
+    :count="count"
+    :outlined="false"
+    v-bind="$attrs"
+    v-on="$listeners"
+  >
+    <slot name="prepend" />
+    {{ text }}
+    <slot name="append" />
+    <v-avatar right v-if="loading">
+      <v-progress-circular indeterminate :size="16" :width="2" />
+    </v-avatar>
+  </counter-chip>
+</template>
diff --git a/aleksis/apps/alsijil/frontend/components/extra_marks/ExtraMarks.vue b/aleksis/apps/alsijil/frontend/components/extra_marks/ExtraMarks.vue
new file mode 100644
index 0000000000000000000000000000000000000000..9468ac1dba5d291abd251659586d24aa95fe0278
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/extra_marks/ExtraMarks.vue
@@ -0,0 +1,138 @@
+<script setup>
+import ColorField from "aleksis.core/components/generic/forms/ColorField.vue";
+import InlineCRUDList from "aleksis.core/components/generic/InlineCRUDList.vue";
+</script>
+
+<template>
+  <v-container>
+    <inline-c-r-u-d-list
+      :headers="headers"
+      :i18n-key="i18nKey"
+      create-item-i18n-key="alsijil.extra_marks.create"
+      :gql-query="gqlQuery"
+      :gql-create-mutation="gqlCreateMutation"
+      :gql-patch-mutation="gqlPatchMutation"
+      :gql-delete-mutation="gqlDeleteMutation"
+      :default-item="defaultItem"
+    >
+      <!-- eslint-disable-next-line vue/valid-v-slot -->
+      <template #name.field="{ attrs, on }">
+        <div aria-required="true">
+          <v-text-field
+            v-bind="attrs"
+            v-on="on"
+            :rules="$rules().required.build()"
+          />
+        </div>
+      </template>
+
+      <!-- eslint-disable-next-line vue/valid-v-slot -->
+      <template #shortName.field="{ attrs, on }">
+        <div aria-required="true">
+          <v-text-field
+            v-bind="attrs"
+            v-on="on"
+            :rules="$rules().required.build()"
+          />
+        </div>
+      </template>
+
+      <template #colourFg="{ item }">
+        <v-chip :color="item.colourFg" outlined v-if="item.colourFg">
+          {{ item.colourFg }}
+        </v-chip>
+        <span v-else>–</span>
+      </template>
+      <!-- eslint-disable-next-line vue/valid-v-slot -->
+      <template #colourFg.field="{ attrs, on }">
+        <color-field v-bind="attrs" v-on="on" />
+      </template>
+
+      <template #colourBg="{ item }">
+        <v-chip :color="item.colourBg" outlined v-if="item.colourBg">
+          {{ item.colourBg }}
+        </v-chip>
+        <span v-else>–</span>
+      </template>
+      <!-- eslint-disable-next-line vue/valid-v-slot -->
+      <template #colourBg.field="{ attrs, on }">
+        <color-field v-bind="attrs" v-on="on" />
+      </template>
+
+      <template #showInCoursebook="{ item }">
+        <v-switch
+          :input-value="item.showInCoursebook"
+          disabled
+          inset
+          :false-value="false"
+          :true-value="true"
+        />
+      </template>
+      <!-- eslint-disable-next-line vue/valid-v-slot -->
+      <template #showInCoursebook.field="{ attrs, on }">
+        <v-switch
+          v-bind="attrs"
+          v-on="on"
+          inset
+          :false-value="false"
+          :true-value="true"
+          :hint="$t('alsijil.extra_marks.show_in_coursebook_helptext')"
+          persistent-hint
+        />
+      </template>
+    </inline-c-r-u-d-list>
+  </v-container>
+</template>
+
+<script>
+import formRulesMixin from "aleksis.core/mixins/formRulesMixin.js";
+import {
+  extraMarks,
+  createExtraMarks,
+  deleteExtraMarks,
+  updateExtraMarks,
+} from "./extra_marks.graphql";
+
+export default {
+  name: "ExtraMarks",
+  mixins: [formRulesMixin],
+  data() {
+    return {
+      headers: [
+        {
+          text: this.$t("alsijil.extra_marks.short_name"),
+          value: "shortName",
+        },
+        {
+          text: this.$t("alsijil.extra_marks.name"),
+          value: "name",
+        },
+        {
+          text: this.$t("alsijil.extra_marks.colour_fg"),
+          value: "colourFg",
+        },
+        {
+          text: this.$t("alsijil.extra_marks.colour_bg"),
+          value: "colourBg",
+        },
+        {
+          text: this.$t("alsijil.extra_marks.show_in_coursebook"),
+          value: "showInCoursebook",
+        },
+      ],
+      i18nKey: "alsijil.extra_marks",
+      gqlQuery: extraMarks,
+      gqlCreateMutation: createExtraMarks,
+      gqlPatchMutation: updateExtraMarks,
+      gqlDeleteMutation: deleteExtraMarks,
+      defaultItem: {
+        shortName: "",
+        name: "",
+        colourFg: "",
+        colourBg: "",
+        showInCoursebook: true,
+      },
+    };
+  },
+};
+</script>
diff --git a/aleksis/apps/alsijil/frontend/components/extra_marks/extra_marks.graphql b/aleksis/apps/alsijil/frontend/components/extra_marks/extra_marks.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..73e8ba4121c215dc4c3968b3ed2021b71b5ecfc1
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/extra_marks/extra_marks.graphql
@@ -0,0 +1,48 @@
+query extraMarks($orderBy: [String], $filters: JSONString) {
+  items: extraMarks(orderBy: $orderBy, filters: $filters) {
+    id
+    shortName
+    name
+    colourFg
+    colourBg
+    showInCoursebook
+    canEdit
+    canDelete
+  }
+}
+
+mutation createExtraMarks($input: [BatchCreateExtraMarkInput]!) {
+  createExtraMarks(input: $input) {
+    items: extraMarks {
+      id
+      shortName
+      name
+      colourFg
+      colourBg
+      showInCoursebook
+      canEdit
+      canDelete
+    }
+  }
+}
+
+mutation deleteExtraMarks($ids: [ID]!) {
+  deleteExtraMarks(ids: $ids) {
+    deletionCount
+  }
+}
+
+mutation updateExtraMarks($input: [BatchPatchExtraMarkInput]!) {
+  updateExtraMarks(input: $input) {
+    items: extraMarks {
+      id
+      shortName
+      name
+      colourFg
+      colourBg
+      showInCoursebook
+      canEdit
+      canDelete
+    }
+  }
+}
diff --git a/aleksis/apps/alsijil/frontend/index.js b/aleksis/apps/alsijil/frontend/index.js
index 1ec79280e06ef377c4be215c27b5291c51043522..e73b19d83631d0953a269f037fc064257bb187d4 100644
--- a/aleksis/apps/alsijil/frontend/index.js
+++ b/aleksis/apps/alsijil/frontend/index.js
@@ -16,45 +16,6 @@ export default {
     byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
   },
   children: [
-    {
-      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",
-        iconActive: "mdi-label-variant",
-        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: "coursebook/",
       component: () => import("./components/coursebook/Coursebook.vue"),
@@ -91,5 +52,17 @@ export default {
         },
       ],
     },
+    {
+      path: "extra_marks/",
+      component: () => import("./components/extra_marks/ExtraMarks.vue"),
+      name: "alsijil.extraMarks",
+      meta: {
+        inMenu: true,
+        titleKey: "alsijil.extra_marks.menu_title",
+        icon: "mdi-label-variant-outline",
+        iconActive: "mdi-label-variant",
+        permission: "alsijil.view_extramarks_rule",
+      },
+    },
   ],
 };
diff --git a/aleksis/apps/alsijil/frontend/messages/de.json b/aleksis/apps/alsijil/frontend/messages/de.json
index 321fddf6acbb2da81cbad7752733859e0323ca0e..e47b2b78a0b7e9b9de790156b3182fbe6c4b3226 100644
--- a/aleksis/apps/alsijil/frontend/messages/de.json
+++ b/aleksis/apps/alsijil/frontend/messages/de.json
@@ -64,6 +64,8 @@
     },
     "extra_marks": {
       "menu_title": "Zusätzliche Markierungen",
+      "title": "Zusätzliche Markierung",
+      "title_plural": "Zusätzliche Markierungen",
       "create": "Markierung erstellen",
       "name": "Markierung",
       "short_name": "Abkürzung",
@@ -75,8 +77,13 @@
     "personal_notes": {
       "note": "Notiz",
       "create_personal_note": "Weitere Notiz",
-      "confirm_delete": "Notiz wirklich löschen?",
-      "confirm_delete_explanation": "Die Notiz \"{note}\" für {name} wird entfernt."
+      "tardiness": "Verspätung",
+      "late": "Verspätet",
+      "minutes_late": "pünktlich | eine Minute verspätet | {n} Minuten zu spät",
+      "lesson_length_exceeded": "Die Verspätung überschreitet die Stundenlänge.",
+      "confirm_delete": "Anmerkung wirklich löschen?",
+      "confirm_delete_explanation": "Die Notiz \"{note}\" für {name} wird entfernt.",
+      "confirm_delete_tardiness": "Die Verspätung von {name} in Höhe von {tardiness} Minuten wird entfernt."
     },
     "group_roles": {
       "menu_title_assign": "Gruppenrollen zuweisen",
@@ -101,5 +108,8 @@
   },
   "actions": {
     "back_to_overview": "Zurück zur Übersicht"
+  },
+  "time": {
+    "minutes": "Minuten"
   }
 }
diff --git a/aleksis/apps/alsijil/frontend/messages/en.json b/aleksis/apps/alsijil/frontend/messages/en.json
index ccbda8b436044c8505397c005ebf3452c01a9b94..3ad99b40cadee92070cf5a2b1ab287343b2a8e6f 100644
--- a/aleksis/apps/alsijil/frontend/messages/en.json
+++ b/aleksis/apps/alsijil/frontend/messages/en.json
@@ -19,7 +19,16 @@
       "menu_title": "My overview"
     },
     "extra_marks": {
-      "menu_title": "Extra marks"
+      "menu_title": "Extra marks",
+      "title": "Extra mark",
+      "title_plural": "Extra marks",
+      "create": "Create Extra mark",
+      "name": "Mark",
+      "short_name": "Abbreviation",
+      "colour_fg": "Foreground Colour",
+      "colour_bg": "Background Colour",
+      "show_in_coursebook": "Show in Coursebook Overview",
+      "show_in_coursebook_helptext": "When checked, this extra mark will be displayed in the lesson summary in the coursebook"
     },
     "excuse_types": {
       "menu_title": "Excuse types"
@@ -85,9 +94,23 @@
         "success": "The absences were registered successfully.",
         "warning": "The following lessons are in the selected time period. Please check that you want to register the absences for these lessons before confirming."
       }
+    },
+    "personal_notes": {
+      "note": "Note",
+      "create_personal_note": "Add another note",
+      "tardiness": "Tardiness",
+      "late": "Late",
+      "minutes_late": "on time | one minute late | {n} minutes late",
+      "lesson_length_exceeded": "The tardiness exceeds the length of the lesson",
+      "confirm_delete": "Delete note?",
+      "confirm_delete_explanation": "The note \"{note}\" for {name} will be removed.",
+      "confirm_delete_tardiness": "The tardiness of {tardiness} minutes will be removed for {name}."
     }
   },
   "actions": {
     "back_to_overview": "Back to overview"
+  },
+  "time": {
+    "minutes": "minutes"
   }
 }
diff --git a/aleksis/apps/alsijil/migrations/0023_add_tardiness_and_rework_constraints.py b/aleksis/apps/alsijil/migrations/0023_add_tardiness_and_rework_constraints.py
new file mode 100644
index 0000000000000000000000000000000000000000..3ca24f5865ddf5d3fb02acbd0aa7522bbc8bda13
--- /dev/null
+++ b/aleksis/apps/alsijil/migrations/0023_add_tardiness_and_rework_constraints.py
@@ -0,0 +1,67 @@
+# Generated by Django 4.2.10 on 2024-07-01 13:44
+# Updated for more custom logic
+
+from django.db import migrations, models
+
+
+def forwards__unique_extra_mark_documentation(apps, schema_editor):
+    NewPersonalNote = apps.get_model("alsijil", "NewPersonalNote")  # noqa
+    db_alias = schema_editor.connection.alias
+
+    duplicates = (NewPersonalNote.objects.using(db_alias)
+                  .values("documentation", "extra_mark", "person")
+                  .annotate(count=models.Count("id"))
+                  .filter(count__gt=1, extra_mark__isnull=False))
+
+    # Iterate over duplicates and delete the extra instances
+    for duplicate in duplicates:
+        pks = (NewPersonalNote
+               .objects
+               .using(db_alias)
+               .filter(person=duplicate["person"], documentation=duplicate["documentation"], extra_mark=duplicate["extra_mark"])
+               .values_list("pk", flat=True)
+               )[1:]
+        NewPersonalNote.objects.using(db_alias).filter(pk__in=pks).delete()
+
+
+def reverse__unique_extra_mark_documentation(apps, schema_editor):
+    # Nothing to do, we cannot bring back the deleted objects, but they were duplicate data, so they are not needed anyway.
+    pass
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('alsijil', '0022_documentation_participation_touched_at'),
+    ]
+
+    operations = [
+        migrations.RemoveConstraint(
+            model_name='newpersonalnote',
+            name='unique_absence_per_documentation',
+        ),
+        migrations.AddField(
+            model_name='participationstatus',
+            name='tardiness',
+            field=models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Tardiness'),
+        ),
+        migrations.AlterField(
+            model_name='newpersonalnote',
+            name='note',
+            field=models.TextField(blank=True, default='', verbose_name='Note'),
+        ),
+        migrations.AddConstraint(
+            model_name='newpersonalnote',
+            constraint=models.CheckConstraint(
+                check=models.Q(models.Q(('note', ''), _negated=True), ('extra_mark__isnull', False), _connector='OR'),
+                name='either_note_or_extra_mark_per_note'),
+        ),
+        migrations.RunPython(forwards__unique_extra_mark_documentation, reverse__unique_extra_mark_documentation),
+        migrations.AddConstraint(
+            model_name='newpersonalnote',
+            constraint=models.UniqueConstraint(
+                condition=models.Q(('extra_mark', None), _negated=True),
+                fields=('person', 'documentation', 'extra_mark'),
+                name='unique_person_documentation_extra_mark',
+                violation_error_message='A person got assigned the same extra mark multiple times per documentation.'),
+        ),
+    ]
diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py
index 6be1734a116c928e11a8a9e8933470d6c2683965..cca46c9cb755c4a116c3e157484b265b8f4f4ae4 100644
--- a/aleksis/apps/alsijil/models.py
+++ b/aleksis/apps/alsijil/models.py
@@ -708,6 +708,7 @@ class Documentation(CalendarEvent):
             self.participation_touched_at
             or not self.amends
             or self.value_start_datetime(self) > now()
+            or self.amends.cancelled
         ):
             # There is no source to update from or it's too early
             return
@@ -803,6 +804,8 @@ class ParticipationStatus(CalendarEvent):
         verbose_name=_("Base Absence"),
     )
 
+    tardiness = models.PositiveSmallIntegerField(blank=True, null=True, verbose_name=_("Tardiness"))
+
     @classmethod
     def get_objects(
         cls, request: HttpRequest | None = None, params: dict[str, any] | None = None
@@ -869,7 +872,7 @@ class NewPersonalNote(ExtensibleModel):
         null=True,
     )
 
-    note = models.TextField(blank=True, verbose_name=_("Note"))
+    note = models.TextField(blank=True, default="", verbose_name=_("Note"))
     extra_mark = models.ForeignKey(
         ExtraMark, on_delete=models.PROTECT, blank=True, null=True, verbose_name=_("Extra Mark")
     )
@@ -881,9 +884,18 @@ class NewPersonalNote(ExtensibleModel):
         verbose_name = _("Personal Note")
         verbose_name_plural = _("Personal Notes")
         constraints = [
+            # This constraint could be dropped in future scenarios
             models.CheckConstraint(
                 check=~Q(note="") | Q(extra_mark__isnull=False),
-                name="unique_absence_per_documentation",
+                name="either_note_or_extra_mark_per_note",
+            ),
+            models.UniqueConstraint(
+                fields=["person", "documentation", "extra_mark"],
+                name="unique_person_documentation_extra_mark",
+                violation_error_message=_(
+                    "A person got assigned the same extra mark multiple times per documentation."
+                ),
+                condition=~Q(extra_mark=None),
             ),
         ]
 
diff --git a/aleksis/apps/alsijil/rules.py b/aleksis/apps/alsijil/rules.py
index 836c7383f2aa37314a2d31e975dfa81c02bf7936..c1ffafbe856ffa64c91a17e3eaf062942debbe40 100644
--- a/aleksis/apps/alsijil/rules.py
+++ b/aleksis/apps/alsijil/rules.py
@@ -13,9 +13,11 @@ from aleksis.core.util.predicates import (
 from .util.predicates import (
     can_edit_documentation,
     can_edit_participation_status,
+    can_edit_personal_note,
     can_view_any_documentation,
     can_view_documentation,
     can_view_participation_status,
+    can_view_personal_note,
     has_lesson_group_object_perm,
     has_person_group_object_perm,
     has_personal_note_group_perm,
@@ -278,6 +280,10 @@ add_perm("alsijil.delete_excusetype_rule", delete_excusetype_predicate)
 view_extramarks_predicate = has_person & has_global_perm("alsijil.view_extramark")
 add_perm("alsijil.view_extramarks_rule", view_extramarks_predicate)
 
+# Fetch all extra marks
+fetch_extramarks_predicate = has_person
+add_perm("alsijil.fetch_extramarks_rule", fetch_extramarks_predicate)
+
 # Add extra mark
 add_extramark_predicate = view_extramarks_predicate & has_global_perm("alsijil.add_extramark")
 add_perm("alsijil.add_extramark_rule", add_extramark_predicate)
@@ -432,3 +438,21 @@ add_perm(
     "alsijil.edit_participation_status_for_documentation_rule",
     edit_participation_status_for_documentation_predicate,
 )
+
+view_personal_note_predicate = has_person & (
+    has_global_perm("alsijil.change_newpersonalnote") | can_view_personal_note
+)
+add_perm(
+    "alsijil.view_personal_note_rule",
+    view_personal_note_predicate,
+)
+
+edit_personal_note_predicate = (
+    has_person
+    & (has_global_perm("alsijil.change_newpersonalnote") | can_edit_personal_note)
+    & is_in_allowed_time_range
+)
+add_perm(
+    "alsijil.edit_personal_note_rule",
+    edit_personal_note_predicate,
+)
diff --git a/aleksis/apps/alsijil/schema/__init__.py b/aleksis/apps/alsijil/schema/__init__.py
index ec8a9b025067e53126b3642a295caa4b10c492bd..9b715583e32a391f0888b0d54d1cb29c8d5e4f79 100644
--- a/aleksis/apps/alsijil/schema/__init__.py
+++ b/aleksis/apps/alsijil/schema/__init__.py
@@ -24,7 +24,18 @@ from .documentation import (
     LessonsForPersonType,
     TouchDocumentationMutation,
 )
+from .extra_marks import (
+    ExtraMarkBatchCreateMutation,
+    ExtraMarkBatchDeleteMutation,
+    ExtraMarkBatchPatchMutation,
+    ExtraMarkType,
+)
 from .participation_status import ParticipationStatusBatchPatchMutation
+from .personal_note import (
+    PersonalNoteBatchCreateMutation,
+    PersonalNoteBatchDeleteMutation,
+    PersonalNoteBatchPatchMutation,
+)
 
 
 class Query(graphene.ObjectType):
@@ -53,6 +64,8 @@ class Query(graphene.ObjectType):
         end=graphene.Date(required=True),
     )
 
+    extra_marks = FilterOrderList(ExtraMarkType)
+
     def resolve_documentations_by_course_id(root, info, course_id, **kwargs):
         documentations = Documentation.objects.filter(
             Q(course__pk=course_id) | Q(amends__course__pk=course_id)
@@ -132,8 +145,10 @@ class Query(graphene.ObjectType):
         else:
             raise PermissionDenied()
 
-        return Group.objects.filter(
-            Q(members=person) | Q(owners=person) | Q(parent_groups__owners=person)
+        return (
+            Group.objects.for_current_school_term_or_all()
+            .filter(Q(members=person) | Q(owners=person) | Q(parent_groups__owners=person))
+            .distinct()
         )
 
     @staticmethod
@@ -146,13 +161,15 @@ class Query(graphene.ObjectType):
             person = info.context.user.person
         else:
             raise PermissionDenied()
-
         return Course.objects.filter(
-            Q(teachers=person)
-            | Q(groups__members=person)
-            | Q(groups__owners=person)
-            | Q(groups__parent_groups__owners=person)
-        )
+            (
+                Q(teachers=person)
+                | Q(groups__members=person)
+                | Q(groups__owners=person)
+                | Q(groups__parent_groups__owners=person)
+            )
+            & Q(groups__in=Group.objects.for_current_school_term_or_all())
+        ).distinct()
 
     @staticmethod
     def resolve_absence_creation_persons(root, info, **kwargs):
@@ -188,3 +205,11 @@ class Mutation(graphene.ObjectType):
     touch_documentation = TouchDocumentationMutation.Field()
     update_participation_statuses = ParticipationStatusBatchPatchMutation.Field()
     create_absences_for_persons = AbsencesForPersonsCreateMutation.Field()
+
+    create_extra_marks = ExtraMarkBatchCreateMutation.Field()
+    update_extra_marks = ExtraMarkBatchPatchMutation.Field()
+    delete_extra_marks = ExtraMarkBatchDeleteMutation.Field()
+
+    create_personal_notes = PersonalNoteBatchCreateMutation.Field()
+    update_personal_notes = PersonalNoteBatchPatchMutation.Field()
+    delete_personal_notes = PersonalNoteBatchDeleteMutation.Field()
diff --git a/aleksis/apps/alsijil/schema/extra_marks.py b/aleksis/apps/alsijil/schema/extra_marks.py
new file mode 100644
index 0000000000000000000000000000000000000000..2b1e3723d2c559e70ddd6793f5f2a89d9c1e6abe
--- /dev/null
+++ b/aleksis/apps/alsijil/schema/extra_marks.py
@@ -0,0 +1,66 @@
+from django.core.exceptions import PermissionDenied
+
+from graphene_django import DjangoObjectType
+
+from aleksis.apps.alsijil.models import ExtraMark
+from aleksis.core.schema.base import (
+    BaseBatchCreateMutation,
+    BaseBatchDeleteMutation,
+    BaseBatchPatchMutation,
+    DjangoFilterMixin,
+    OptimisticResponseTypeMixin,
+    PermissionsTypeMixin,
+)
+
+
+class ExtraMarkType(
+    OptimisticResponseTypeMixin,
+    PermissionsTypeMixin,
+    DjangoFilterMixin,
+    DjangoObjectType,
+):
+    class Meta:
+        model = ExtraMark
+        fields = ("id", "short_name", "name", "colour_fg", "colour_bg", "show_in_coursebook")
+
+    @classmethod
+    def get_queryset(cls, queryset, info):
+        if info.context.user.has_perm("alsijil.fetch_extramarks_rule"):
+            return queryset
+        raise PermissionDenied()
+
+
+class ExtraMarkBatchCreateMutation(BaseBatchCreateMutation):
+    class Meta:
+        model = ExtraMark
+        fields = ("short_name", "name", "colour_fg", "colour_bg", "show_in_coursebook")
+        optional_fields = ("name",)
+
+    @classmethod
+    def check_permissions(cls, root, info, input):  # noqa
+        if info.context.user.has_perm("alsijil.create_extramark_rule"):
+            return
+        raise PermissionDenied()
+
+
+class ExtraMarkBatchDeleteMutation(BaseBatchDeleteMutation):
+    class Meta:
+        model = ExtraMark
+
+    @classmethod
+    def check_permissions(cls, root, info, input):  # noqa
+        if info.context.user.has_perm("alsijil.delete_extramark_rule"):
+            return
+        raise PermissionDenied()
+
+
+class ExtraMarkBatchPatchMutation(BaseBatchPatchMutation):
+    class Meta:
+        model = ExtraMark
+        fields = ("id", "short_name", "name", "colour_fg", "colour_bg", "show_in_coursebook")
+
+    @classmethod
+    def check_permissions(cls, root, info, input):  # noqa
+        if info.context.user.has_perm("alsijil.edit_extramark_rule"):
+            return
+        raise PermissionDenied()
diff --git a/aleksis/apps/alsijil/schema/participation_status.py b/aleksis/apps/alsijil/schema/participation_status.py
index 246ae52a0aab7e7bf57f102ccd7e6558d4639496..335f5bc8d1c22682725563d2c3c83b88032fbc38 100644
--- a/aleksis/apps/alsijil/schema/participation_status.py
+++ b/aleksis/apps/alsijil/schema/participation_status.py
@@ -1,8 +1,10 @@
 from django.core.exceptions import PermissionDenied
 
+import graphene
 from graphene_django import DjangoObjectType
 
-from aleksis.apps.alsijil.models import ParticipationStatus
+from aleksis.apps.alsijil.models import NewPersonalNote, ParticipationStatus
+from aleksis.apps.alsijil.schema.personal_note import PersonalNoteType
 from aleksis.core.schema.base import (
     BaseBatchPatchMutation,
     DjangoFilterMixin,
@@ -25,13 +27,37 @@ class ParticipationStatusType(
             "absence_reason",
             "related_documentation",
             "base_absence",
+            "tardiness",
+        )
+
+    notes_with_extra_mark = graphene.List(PersonalNoteType)
+    notes_with_note = graphene.List(PersonalNoteType)
+
+    @staticmethod
+    def resolve_notes_with_extra_mark(root: ParticipationStatus, info, **kwargs):
+        return NewPersonalNote.objects.filter(
+            person=root.person,
+            documentation=root.related_documentation,
+            extra_mark__isnull=False,
+        )
+
+    @staticmethod
+    def resolve_notes_with_note(root: ParticipationStatus, info, **kwargs):
+        return NewPersonalNote.objects.filter(
+            person=root.person,
+            documentation=root.related_documentation,
+            note__isnull=False,
         )
 
 
 class ParticipationStatusBatchPatchMutation(BaseBatchPatchMutation):
     class Meta:
         model = ParticipationStatus
-        fields = ("id", "absence_reason")  # Only the reason can be updated after creation
+        fields = (
+            "id",
+            "absence_reason",
+            "tardiness",
+        )  # Only the reason and tardiness can be updated after creation
         return_field_name = "participationStatuses"
 
     @classmethod
diff --git a/aleksis/apps/alsijil/schema/personal_note.py b/aleksis/apps/alsijil/schema/personal_note.py
new file mode 100644
index 0000000000000000000000000000000000000000..dfe36359d87345a0b51992e689627065638fcd4e
--- /dev/null
+++ b/aleksis/apps/alsijil/schema/personal_note.py
@@ -0,0 +1,50 @@
+from graphene_django import DjangoObjectType
+
+from aleksis.apps.alsijil.models import NewPersonalNote
+from aleksis.core.schema.base import (
+    BaseBatchCreateMutation,
+    BaseBatchDeleteMutation,
+    BaseBatchPatchMutation,
+    DjangoFilterMixin,
+    OptimisticResponseTypeMixin,
+    PermissionsTypeMixin,
+)
+
+
+class PersonalNoteType(
+    OptimisticResponseTypeMixin,
+    PermissionsTypeMixin,
+    DjangoFilterMixin,
+    DjangoObjectType,
+):
+    class Meta:
+        model = NewPersonalNote
+        fields = (
+            "id",
+            "note",
+            "extra_mark",
+        )
+
+
+class PersonalNoteBatchCreateMutation(BaseBatchCreateMutation):
+    class Meta:
+        model = NewPersonalNote
+        type_name = "BatchCreatePersonalNoteInput"
+        return_field_name = "personalNotes"
+        fields = ("note", "extra_mark", "documentation", "person")
+        permissions = ("alsijil.edit_personal_note_rule",)
+
+
+class PersonalNoteBatchPatchMutation(BaseBatchPatchMutation):
+    class Meta:
+        model = NewPersonalNote
+        type_name = "BatchPatchPersonalNoteInput"
+        return_field_name = "personalNotes"
+        fields = ("id", "note", "extra_mark", "documentation", "person")
+        permissions = ("alsijil.edit_personal_note_rule",)
+
+
+class PersonalNoteBatchDeleteMutation(BaseBatchDeleteMutation):
+    class Meta:
+        model = NewPersonalNote
+        permissions = ("alsijil.edit_personal_note_rule",)
diff --git a/aleksis/apps/alsijil/tables.py b/aleksis/apps/alsijil/tables.py
index a0ef1734e5eea326fd9a8fd0f858b71a89089082..f17a214930f2d7f2e40a1662b491eab587bda0ad 100644
--- a/aleksis/apps/alsijil/tables.py
+++ b/aleksis/apps/alsijil/tables.py
@@ -11,26 +11,6 @@ from aleksis.core.util.tables import SelectColumn
 from .models import PersonalNote
 
 
-class ExtraMarkTable(tables.Table):
-    class Meta:
-        attrs = {"class": "highlight"}
-
-    name = tables.LinkColumn("edit_extra_mark", args=[A("id")])
-    short_name = tables.Column()
-    edit = tables.LinkColumn(
-        "edit_extra_mark",
-        args=[A("id")],
-        text=_("Edit"),
-        attrs={"a": {"class": "btn-flat waves-effect waves-orange orange-text"}},
-    )
-    delete = tables.LinkColumn(
-        "delete_extra_mark",
-        args=[A("id")],
-        text=_("Delete"),
-        attrs={"a": {"class": "btn-flat waves-effect waves-red red-text"}},
-    )
-
-
 class ExcuseTypeTable(tables.Table):
     class Meta:
         attrs = {"class": "highlight"}
diff --git a/aleksis/apps/alsijil/templates/alsijil/extra_mark/create.html b/aleksis/apps/alsijil/templates/alsijil/extra_mark/create.html
deleted file mode 100644
index d0ee3a9055561df1f468692f79d794404a60c1d1..0000000000000000000000000000000000000000
--- a/aleksis/apps/alsijil/templates/alsijil/extra_mark/create.html
+++ /dev/null
@@ -1,17 +0,0 @@
-	{# -*- engine:django -*- #}
-
-	{% extends "core/base.html" %}
-	{% load material_form i18n %}
-
-	{% block browser_title %}{% blocktrans %}Create extra mark{% endblocktrans %}{% endblock %}
-	{% block page_title %}{% blocktrans %}Create extra mark{% endblocktrans %}{% endblock %}
-
-	{% block content %}
-
-	  <form method="post">
-	    {% csrf_token %}
-	    {% form form=form %}{% endform %}
-	    {% include "core/partials/save_button.html" %}
-	  </form>
-
-	{% endblock %}
diff --git a/aleksis/apps/alsijil/templates/alsijil/extra_mark/edit.html b/aleksis/apps/alsijil/templates/alsijil/extra_mark/edit.html
deleted file mode 100644
index 7adee30a1cfd30256d70b2a823827384de331e05..0000000000000000000000000000000000000000
--- a/aleksis/apps/alsijil/templates/alsijil/extra_mark/edit.html
+++ /dev/null
@@ -1,17 +0,0 @@
-{# -*- engine:django -*- #}
-
-{% extends "core/base.html" %}
-{% load material_form i18n %}
-
-{% block browser_title %}{% blocktrans %}Edit extra mark{% endblocktrans %}{% endblock %}
-{% block page_title %}{% blocktrans %}Edit extra mark{% endblocktrans %}{% endblock %}
-
-{% block content %}
-
-  <form method="post">
-    {% csrf_token %}
-    {% form form=form %}{% endform %}
-    {% include "core/partials/save_button.html" %}
-  </form>
-
-{% endblock %}
diff --git a/aleksis/apps/alsijil/templates/alsijil/extra_mark/list.html b/aleksis/apps/alsijil/templates/alsijil/extra_mark/list.html
deleted file mode 100644
index 9eeb63b1a81162e6490072ca7184059be7a5193a..0000000000000000000000000000000000000000
--- a/aleksis/apps/alsijil/templates/alsijil/extra_mark/list.html
+++ /dev/null
@@ -1,18 +0,0 @@
-{# -*- engine:django -*- #}
-
-{% extends "core/base.html" %}
-
-{% load i18n %}
-{% load render_table from django_tables2 %}
-
-{% block browser_title %}{% blocktrans %}Extra marks{% endblocktrans %}{% endblock %}
-{% block page_title %}{% blocktrans %}Extra marks{% endblocktrans %}{% endblock %}
-
-{% block content %}
-  <a class="btn green waves-effect waves-light" href="{% url 'create_extra_mark' %}">
-    <i class="material-icons iconify left" data-icon="mdi:plus"></i>
-    {% trans "Create extra mark" %}
-  </a>
-
-  {% render_table table %}
-{% endblock %}
diff --git a/aleksis/apps/alsijil/urls.py b/aleksis/apps/alsijil/urls.py
index edc0b4fa81cfb50e1f268d9dd21b3d9dda2c033a..cd7367ce84973bb4eb62791e9ce5b2c7eb3a5f85 100644
--- a/aleksis/apps/alsijil/urls.py
+++ b/aleksis/apps/alsijil/urls.py
@@ -3,22 +3,6 @@ from django.urls import path
 from . import views
 
 urlpatterns = [
-    path("extra_marks/", views.ExtraMarkListView.as_view(), name="extra_marks"),
-    path(
-        "extra_marks/create/",
-        views.ExtraMarkCreateView.as_view(),
-        name="create_extra_mark",
-    ),
-    path(
-        "extra_marks/<int:pk>/edit/",
-        views.ExtraMarkEditView.as_view(),
-        name="edit_extra_mark",
-    ),
-    path(
-        "extra_marks/<int:pk>/delete/",
-        views.ExtraMarkDeleteView.as_view(),
-        name="delete_extra_mark",
-    ),
     path("group_roles/", views.GroupRoleListView.as_view(), name="group_roles"),
     path("group_roles/create/", views.GroupRoleCreateView.as_view(), name="create_group_role"),
     path(
diff --git a/aleksis/apps/alsijil/util/predicates.py b/aleksis/apps/alsijil/util/predicates.py
index 6dea3844a318c6572e63b254e54c846505b8fb4e..e1d8f8a02760b846d35672ffddeeb73d161ece72 100644
--- a/aleksis/apps/alsijil/util/predicates.py
+++ b/aleksis/apps/alsijil/util/predicates.py
@@ -12,7 +12,7 @@ from aleksis.core.models import Group, Person
 from aleksis.core.util.core_helpers import get_site_preferences
 from aleksis.core.util.predicates import check_object_permission
 
-from ..models import Documentation, PersonalNote
+from ..models import Documentation, NewPersonalNote, PersonalNote
 
 
 @predicate
@@ -445,6 +445,8 @@ def can_edit_documentation(user: User, obj: Documentation):
 def can_view_participation_status(user: User, obj: Documentation):
     """Predicate which checks if the user is allowed to view participation for a documentation."""
     if obj:
+        if obj.amends and obj.amends.cancelled:
+            return False
         if is_documentation_teacher(user, obj):
             return True
         if obj.amends:
@@ -460,6 +462,8 @@ def can_view_participation_status(user: User, obj: Documentation):
 def can_edit_participation_status(user: User, obj: Documentation):
     """Predicate which checks if the user is allowed to edit participation for a documentation."""
     if obj:
+        if obj.amends and obj.amends.cancelled:
+            return False
         if is_documentation_teacher(user, obj):
             return True
         if obj.amends:
@@ -470,8 +474,14 @@ def can_edit_participation_status(user: User, obj: Documentation):
 
 
 @predicate
-def is_in_allowed_time_range(user: User, obj: Documentation):
-    """Predicate which checks if the documentation is in the allowed time range for editing."""
+def is_in_allowed_time_range(user: User, obj: Union[Documentation, NewPersonalNote]):
+    """Predicate for documentations or new personal notes with linked documentation.
+
+    Predicate which checks if the given documentation or the documentation linked
+    to the given NewPersonalNote is in the allowed time range for editing.
+    """
+    if isinstance(obj, NewPersonalNote):
+        obj = obj.documentation
     if obj and (
         get_site_preferences()["alsijil__allow_edit_future_documentations"] == "all"
         or (
@@ -493,3 +503,31 @@ def is_in_allowed_time_range_for_participation_status(user: User, obj: Documenta
     if obj and obj.value_start_datetime(obj) <= now():
         return True
     return False
+
+
+@predicate
+def can_view_personal_note(user: User, obj: NewPersonalNote):
+    """Predicate which checks if the user is allowed to view a personal note."""
+    if obj.documentation:
+        if is_documentation_teacher(user, obj.documentation):
+            return True
+        if obj.documentation.amends:
+            return is_lesson_event_teacher(
+                user, obj.documentation.amends
+            ) | is_lesson_event_group_owner(user, obj.documentation.amends)
+        if obj.documentation.course:
+            return is_course_teacher(user, obj.documentation.course)
+    return False
+
+
+@predicate
+def can_edit_personal_note(user: User, obj: NewPersonalNote):
+    """Predicate which checks if the user is allowed to edit a personal note."""
+    if obj.documentation:
+        if is_documentation_teacher(user, obj.documentation):
+            return True
+        if obj.documentation.amends:
+            return is_lesson_event_teacher(
+                user, obj.documentation.amends
+            ) | is_lesson_event_group_owner(user, obj.documentation.amends)
+    return False
diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py
index dd010383b83ae7e28c6d98fd7de34bf9aa5432ba..0395a8bfbc54fcab81dfea092f431045d15ae134 100644
--- a/aleksis/apps/alsijil/views.py
+++ b/aleksis/apps/alsijil/views.py
@@ -48,7 +48,6 @@ from .filters import PersonalNoteFilter
 from .forms import (
     AssignGroupRoleForm,
     ExcuseTypeForm,
-    ExtraMarkForm,
     FilterRegisterObjectForm,
     GroupRoleAssignmentEditForm,
     GroupRoleForm,
@@ -62,7 +61,6 @@ from .forms import (
 from .models import ExcuseType, ExtraMark, GroupRole, GroupRoleAssignment, PersonalNote
 from .tables import (
     ExcuseTypeTable,
-    ExtraMarkTable,
     GroupRoleTable,
     PersonalNoteTable,
     RegisterObjectSelectTable,
@@ -1052,51 +1050,6 @@ class DeletePersonalNoteView(PermissionRequiredMixin, DetailView):
         return redirect("overview_person", note.person.pk)
 
 
-@method_decorator(pwa_cache, "dispatch")
-class ExtraMarkListView(PermissionRequiredMixin, SingleTableView):
-    """Table of all extra marks."""
-
-    model = ExtraMark
-    table_class = ExtraMarkTable
-    permission_required = "alsijil.view_extramarks_rule"
-    template_name = "alsijil/extra_mark/list.html"
-
-
-@method_decorator(never_cache, name="dispatch")
-class ExtraMarkCreateView(PermissionRequiredMixin, AdvancedCreateView):
-    """Create view for extra marks."""
-
-    model = ExtraMark
-    form_class = ExtraMarkForm
-    permission_required = "alsijil.add_extramark_rule"
-    template_name = "alsijil/extra_mark/create.html"
-    success_url = reverse_lazy("extra_marks")
-    success_message = _("The extra mark has been created.")
-
-
-@method_decorator(never_cache, name="dispatch")
-class ExtraMarkEditView(PermissionRequiredMixin, AdvancedEditView):
-    """Edit view for extra marks."""
-
-    model = ExtraMark
-    form_class = ExtraMarkForm
-    permission_required = "alsijil.edit_extramark_rule"
-    template_name = "alsijil/extra_mark/edit.html"
-    success_url = reverse_lazy("extra_marks")
-    success_message = _("The extra mark has been saved.")
-
-
-@method_decorator(never_cache, name="dispatch")
-class ExtraMarkDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView):
-    """Delete view for extra marks."""
-
-    model = ExtraMark
-    permission_required = "alsijil.delete_extramark_rule"
-    template_name = "core/pages/delete.html"
-    success_url = reverse_lazy("extra_marks")
-    success_message = _("The extra mark has been deleted.")
-
-
 @method_decorator(pwa_cache, "dispatch")
 class ExcuseTypeListView(PermissionRequiredMixin, SingleTableView):
     """Table of all excuse types."""