diff --git a/README.rst b/README.rst
index 61e80f73c24aa9077218aa86a2427463f75f1f0f..e2af874fa42f6e23675156e27fee583be1576b1e 100644
--- a/README.rst
+++ b/README.rst
@@ -38,6 +38,7 @@ Licence
   Copyright © 2020, 2021 Julian Leucker <leuckeju@katharineum.de>
   Copyright © 2020, 2022 Hangzhi Yu <yuha@katharineum.de>
   Copyright © 2021 Lloyd Meins <meinsll@katharineum.de>
+  Copyright © 2022 magicfelix <felix@felix-zauberer.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..326976e34995942e7431a579c5a6044b29fe4434 100644
--- a/aleksis/apps/alsijil/apps.py
+++ b/aleksis/apps/alsijil/apps.py
@@ -17,4 +17,5 @@ class AlsijilConfig(AppConfig):
         ([2020, 2021], "Julian Leucker", "leuckeju@katharineum.de"),
         ([2020, 2022], "Hangzhi Yu", "yuha@katharineum.de"),
         ([2021], "Lloyd Meins", "meinsll@katharineum.de"),
+        ([2022], "magicfelix", "felix@felix-zauberer.de"),
     )
diff --git a/aleksis/apps/alsijil/assets/UpdateStatuses.js b/aleksis/apps/alsijil/assets/UpdateStatuses.js
new file mode 100644
index 0000000000000000000000000000000000000000..bb69013839fa3b83cd69d5e036d82507248ed8a1
--- /dev/null
+++ b/aleksis/apps/alsijil/assets/UpdateStatuses.js
@@ -0,0 +1,5 @@
+export const
+    ERROR = "ERROR",         // Something went wrong
+    SAVED = "SAVED",         // Everything alright
+    UPDATING = "UPDATING",   // We are sending something to the server
+    CHANGES = "CHANGES"      // the user changed something, but it has not been saved yet
diff --git a/aleksis/apps/alsijil/assets/components/coursebook/CourseBook.graphql b/aleksis/apps/alsijil/assets/components/coursebook/CourseBook.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..9af1bf9dd9ffb33e16f12ebdd969393f40cab7f1
--- /dev/null
+++ b/aleksis/apps/alsijil/assets/components/coursebook/CourseBook.graphql
@@ -0,0 +1,80 @@
+query CourseBook($lessonId: ID!) {
+  excuseTypes {
+    id
+    name
+    shortName
+  }
+  lesson: lessonById(id: $lessonId) {
+    groups {
+      name
+      shortName
+      members {
+        id
+        fullName
+      }
+    }
+    subject {
+      name
+    }
+    plannedLessonperiodsDatetimes {
+      year
+      week
+      datetimeStart
+      lessonPeriod{
+        id
+        period{
+          period
+        }
+      }
+    }
+  }
+  lessonDocumentations: lessonDocumentationsByLessonId(id: $lessonId) {
+    id
+    topic
+    homework
+    groupNote
+    year
+    week
+    lessonPeriod {
+      id
+      period {
+        id
+        period
+      }
+    }
+    event {
+      id
+    }
+    extraLesson {
+      id
+    }
+    period
+    date
+    personalNotes {
+      id
+      person {
+        id
+        fullName
+      }
+      tardiness
+      absent
+      excused
+      excuseType {
+        id
+        name
+        shortName
+      }
+      remarks
+      extraMarks {
+        id
+        name
+        shortName
+      }
+    }
+  }
+  extraMarks {
+    id
+    name
+    shortName
+  }
+}
diff --git a/aleksis/apps/alsijil/assets/components/coursebook/CourseBook.vue b/aleksis/apps/alsijil/assets/components/coursebook/CourseBook.vue
new file mode 100644
index 0000000000000000000000000000000000000000..6c29cc64e5635cf89b825a0e2809c9bf018eb047
--- /dev/null
+++ b/aleksis/apps/alsijil/assets/components/coursebook/CourseBook.vue
@@ -0,0 +1,85 @@
+<template>
+  <ApolloQuery
+    :query="require('./CourseBook.graphql')"
+    :variables="{ lessonId: $route.params.lessonId }"
+  >
+    <template v-slot="{ result: { loading, error, data } }">
+        <!-- Error -->
+        <message-box v-if="error" type="error">{{ $t("alsijil.error_occurred") }}</message-box>
+    
+        <!-- Result -->
+        <div v-else-if="data" class="result apollo">
+          <div class="d-flex justify-space-between">
+            <v-btn text color="primary" :href="$root.urls.select_coursebook()">
+              <v-icon left>mdi-chevron-left</v-icon>
+              {{ $t("alsijil.back") }}
+            </v-btn>
+            <update-indicator @manual-update="updateManually()" ref="indicator" :status="status"></update-indicator>
+          </div>
+          <v-row>
+            <v-col cols="12">
+              <lesson-documentations
+                :lesson-documentations="data.lessonDocumentations"
+                :planned-lesson-periods-date-times="data.lesson.plannedLessonperiodsDatetimes"
+                :groups="data.lesson.groups"
+                :excuse-types="data.excuseTypes"
+                :extra-marks="data.extraMarks"
+                :save-lesson-documentations-per-week="saveLessonDocumentationsPerWeek"
+              />
+            </v-col>
+          </v-row>
+        </div>
+        <!-- No result or Loading -->
+        <div v-else class="text-center">
+          <v-progress-circular
+            indeterminate
+            color="primary"
+            class="ma-auto"
+          ></v-progress-circular>
+        </div>
+    </template>
+  </ApolloQuery>
+</template>
+
+<script>
+import {CHANGES, SAVED, UPDATING} from "../../UpdateStatuses.js";
+import UpdateIndicator from "./UpdateIndicator.vue";
+import LessonDocumentations from "./LessonDocumentations.vue";
+
+export default {
+    components: {
+        UpdateIndicator,
+        LessonDocumentations,
+    },
+    props: [ "saveLessonDocumentationsPerWeek" ],
+    methods: {
+        processDataChange(event) {
+            this.status = CHANGES;
+            // alert("Probably save the data");
+            console.log(event);
+            setTimeout(() => {
+                this.status = UPDATING;
+            }, 500)
+
+            setTimeout(() => {
+                this.status = SAVED;
+            }, 1000)
+
+        },
+        updateManually(event) {
+            alert("Data sync triggered manually");
+            this.status = UPDATING;
+            setTimeout(() => {
+                this.status = SAVED;
+            }, 500)
+        },
+    },
+    name: "course-book",
+    data: () => {
+        return {
+            ping: "ping",
+            status: SAVED,
+        }
+    }
+}
+</script>
diff --git a/aleksis/apps/alsijil/assets/components/coursebook/LessonDocumentation.graphql b/aleksis/apps/alsijil/assets/components/coursebook/LessonDocumentation.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..c591598ae52bd8a72caa67d3e6e974ae78743257
--- /dev/null
+++ b/aleksis/apps/alsijil/assets/components/coursebook/LessonDocumentation.graphql
@@ -0,0 +1,32 @@
+mutation UpdateOrCreateLessonDocumentation($year:Int!, $week:Int!, $lessonPeriodId:ID, $topic:String, $homework:String, $groupNote:String) {
+  updateOrCreateLessonDocumentation(year:$year, week:$week, lessonPeriodId:$lessonPeriodId, topic:$topic, homework:$homework, groupNote:$groupNote) {
+    lessonDocumentation{
+      id
+      topic
+      homework
+      groupNote
+      date
+      personalNotes {
+        id
+        person {
+          id
+          fullName
+        }
+        tardiness
+        absent
+        excused
+        excuseType {
+          id
+          name
+          shortName
+        }
+        remarks
+        extraMarks {
+          id
+          name
+          shortName
+        }
+      }
+    }
+  }
+}
diff --git a/aleksis/apps/alsijil/assets/components/coursebook/LessonDocumentation.vue b/aleksis/apps/alsijil/assets/components/coursebook/LessonDocumentation.vue
new file mode 100644
index 0000000000000000000000000000000000000000..e2679b4ade3b67b44b24ef60f96f2d024d29aeab
--- /dev/null
+++ b/aleksis/apps/alsijil/assets/components/coursebook/LessonDocumentation.vue
@@ -0,0 +1,160 @@
+<template>
+  <ApolloMutation
+      :mutation="require('./LessonDocumentation.graphql')"
+      :variables=lessonDocumentationEdit
+      @done="onDone"
+  >
+    <template v-slot="{ mutate, loading, error }">
+      <v-card elevation="2" :loading="loading">
+        <v-form v-model="valid">
+          <v-card-title v-if="saveLessonDocumentationsPerWeek === 'True'">
+            <span
+              v-text="getWeekText(lessonDocumentationEdit)"
+              class="ma-1 text-h5">
+            </span>
+          </v-card-title>
+          <v-card-title v-else>
+            <v-hover v-slot="{ hover }">
+              <div>
+                <v-menu
+                    v-model="showPicker"
+                    :close-on-content-click="false"
+                    transition="scale-transition"
+                    offset-y
+                    min-width="auto"
+                >
+                  <template v-slot:activator="{ on, attrs }">
+                    <span>
+                      <span v-text="$d(new Date(lessonDocumentationEdit.date), 'short')"
+                            class="ma-1 text-h5"></span>
+                      <v-btn right v-bind="attrs" v-on="on" icon v-if="hover && dateAndPeriodEditable">
+                        <v-icon>mdi-pencil-outline</v-icon>
+                      </v-btn>
+                    </span>
+                  </template>
+                  <v-date-picker
+                      scrollable
+                      no-title
+                      @input="showPicker = false; $emit('change-date', $event)"
+                      v-model="lessonDocumentationEdit.date"
+                  ></v-date-picker>
+                </v-menu>
+              </div>
+            </v-hover>
+            <v-hover v-slot="{ hover }" v-if="!(saveLessonDocumentationsPerWeek === 'True')">
+              <div>
+                <v-menu offset-y>
+                  <template v-slot:activator="{ on, attrs }">
+                    <span>
+                      <span
+                          v-text="$t('alsijil.period_number', {number: lessonDocumentationEdit.period})"
+                          class="ma-1 text-h5"></span>
+                      <v-btn
+                          right
+                          v-bind="attrs"
+                          v-on="on"
+                          icon
+                          v-if="hover && dateAndPeriodEditable"
+                      >
+                        <v-icon>mdi-pencil-outline</v-icon>
+                      </v-btn>
+                    </span>
+                  </template>
+                  <v-list>
+                    <!-- Fixme: load valid lessons -->
+                    <v-list-item
+                        v-for="(item, index) in [1, 2, 3, 4, 5, 6, 7, 8, 9]"
+                        :key="index"
+                    >
+                      <v-list-item-title>{{ item }}</v-list-item-title>
+                    </v-list-item>
+                  </v-list>
+                </v-menu>
+              </div>
+            </v-hover>
+          </v-card-title>
+          <v-card-text>
+            <v-row>
+              <v-col cols="12" md="12" lg="12">
+                <message-box type="error" v-if="error">{{ $t("alsijil.error_updating") }}</message-box>
+                <v-textarea
+                    name="input-7-1"
+                    :label="$t('alsijil.lesson_documentation.topic')"
+                    rows="1"
+                    auto-grow
+                    required
+
+                    v-model="lessonDocumentationEdit.topic"
+                ></v-textarea>
+                <v-textarea
+                    name="input-7-1"
+                    :label="$t('alsijil.lesson_documentation.homework')"
+                    rows="1"
+                    auto-grow
+
+                    v-model="lessonDocumentationEdit.homework"
+                ></v-textarea>
+                <v-textarea
+                    name="input-7-1"
+                    :label="$t('alsijil.lesson_documentation.group_note')"
+                    rows="1"
+                    auto-grow
+
+                    v-model="lessonDocumentationEdit.groupNote"
+                ></v-textarea>
+              </v-col>
+              <v-col v-if="!(saveLessonDocumentationsPerWeek === 'True')" cols="12" md="4" lg="4">
+                Personal notes
+                <personal-notes
+                    :lesson-documentation-id="lessonDocumentationEdit.id"
+                    :groups="groups"
+                    :excuse-types="excuseTypes"
+                    :extra-marks="extraMarks"
+
+                    v-model="lessonDocumentationEdit.personalNotes"
+                    @change="$emit('change-personal-notes', $event)"
+                ></personal-notes>
+              </v-col>
+            </v-row>
+          </v-card-text>
+          <v-card-actions>
+            <v-spacer></v-spacer>
+            <v-btn
+                color="error"
+                outlined
+                @click="$emit('cancel-lesson-documentation-dialog', $event)"
+            >
+              {{ $t('alsijil.cancel') }}
+            </v-btn>
+            <v-btn
+                color="success"
+                @click="mutate()"
+            >
+              {{ $t('alsijil.save') }}
+            </v-btn>
+          </v-card-actions>
+        </v-form>
+      </v-card>
+    </template>
+  </ApolloMutation>
+</template>
+
+<script>
+import PersonalNotes from "./PersonalNotes.vue";
+
+export default {
+  components: {PersonalNotes},
+  props: ["lessonDocumentationEdit", "groups", "excuseTypes", "extraMarks", "saveLessonDocumentationsPerWeek", "getWeekText"],
+  name: "lesson-documentation",
+  data() {
+    return {
+      dateAndPeriodEditable: false,
+      showPicker: false,
+      //lessonDocumentationEdit: {},
+    }
+  },
+  //created() {
+  //this.lessonDocumentationEdit = this.lessonDocumentation
+  //}
+}
+</script>
diff --git a/aleksis/apps/alsijil/assets/components/coursebook/LessonDocumentations.vue b/aleksis/apps/alsijil/assets/components/coursebook/LessonDocumentations.vue
new file mode 100644
index 0000000000000000000000000000000000000000..0fdb51f7768bc8292f65a74ad86d877ef892f5aa
--- /dev/null
+++ b/aleksis/apps/alsijil/assets/components/coursebook/LessonDocumentations.vue
@@ -0,0 +1,283 @@
+<template><div>
+  <v-dialog
+    v-model="dialog"
+    max-width="800"
+  >
+    <template v-slot:activator="{ on, attrs }">
+      <v-row>
+        <v-col cols="12" md="6" class="pb-0 pb-md-3">
+          <v-select
+            v-if="saveLessonDocumentationsPerWeek === 'True'"
+            :items="emptyLessonPeriods"
+            :label="$t('alsijil.coursebook.choose_week')"
+            :item-text="getWeekText"
+            v-model="selectedLessonPeriodDatetime"
+            return-object
+          ></v-select>
+          <v-select
+            v-else
+            :items="emptyLessonPeriods"
+            :label="$t('alsijil.coursebook.choose_lesson_date')"
+            :item-text="getLessonText"
+            v-model="selectedLessonPeriodDatetime"
+            return-object
+          ></v-select>
+        </v-col>
+        <v-col cols="12" md="6" class="pt-0 pt-md-3">
+          <v-btn
+            color="primary"
+            dark
+            v-bind="attrs"
+            v-on="on"
+            @click="createLessonDocumentation()"
+          >
+            {{ $t("alsijil.coursebook.create_documentation") }}
+          </v-btn>
+        </v-col>
+      </v-row>
+    </template>
+    <lesson-documentation
+      :lesson-documentation-edit="lessonDocumentationEdit"
+      :groups="groups"
+      :excuse-types="excuseTypes"
+      :extra-marks="extraMarks"
+      :save-lesson-documentations-per-week="saveLessonDocumentationsPerWeek"
+      :get-week-text="getWeekText"
+      @cancel-lesson-documentation-dialog="cancelDialog"
+    />
+  </v-dialog>
+  <v-data-table
+    :headers="headers"
+    :items="computedLessonDocumentations"
+    @click:row="editLessonDocumentation"
+    class="elevation-1 my-3"
+    :expanded.sync="expanded"
+    show-expand
+    multi-sort
+    :sort-by="['year','week']"
+    :sort-desc="[true, true]"
+  >
+    <template v-slot:item.period="{ item }">
+      <span class="text-no-wrap">{{ (saveLessonDocumentationsPerWeek === "True") ? getWeekText(item) : getLessonText(item) }}</span>
+    </template>
+    <template v-slot:expanded-item="{ headers, item }">
+      <td :colspan="headers.length">
+        <template v-if="saveLessonDocumentationsPerWeek === 'True'" v-for="lessonDocumentation in item.documentations">
+          <v-list-item>
+            <v-list-item-content>
+              <v-list-item-title>{{ getLessonText(lessonDocumentation) }}</v-list-item-title>
+              <v-list-item-action>
+                <personal-notes
+                    :lesson-documentation-id="lessonDocumentation.id"
+                    :groups="groups"
+                    :excuse-types="excuseTypes"
+                    :extra-marks="extraMarks"
+
+                    v-model="lessonDocumentation.personalNotes"
+                    @change="$emit('change-personal-notes', $event)"
+                ></personal-notes>
+              </v-list-item-action>
+            </v-list-item-content>
+          </v-list-item>
+          <v-divider></v-divider>
+        </template>
+        <template v-else v-for="personalNote in item.personalNotes">
+<!--          FIXME: Add edit and delete functionality to personal note chips-->
+          <v-chip class="ma-1" v-if="personalNoteString(personalNote)">
+            {{ personalNote.person.fullName }}: {{ personalNoteString(personalNote) }}
+          </v-chip>
+        </template>
+      </td>
+    </template>
+</v-data-table>
+</div></template>
+
+<script>
+  import LessonDocumentation from "./LessonDocumentation.vue";
+  import PersonalNotes from "./PersonalNotes.vue";
+  export default {
+    components: {LessonDocumentation, PersonalNotes},
+    props: [ "lessonDocumentations", "plannedLessonPeriodsDateTimes",  "groups", "excuseTypes", "extraMarks", "saveLessonDocumentationsPerWeek" ],
+    name: "lesson-documentations",
+    data () {
+      return {
+        dialog: false,
+        expanded: [],
+        headers: [
+          { text: this.$t("alsijil.period"), value: "period" },
+          { text: this.$t("alsijil.lesson_documentation.topic"), value: "topic" },
+          { text: this.$t("alsijil.lesson_documentation.homework"), value: "homework" },
+          { text: this.$t("alsijil.lesson_documentation.group_note"), value: "groupNote" },
+          { text: this.$t("alsijil.personal_note.title_plural"), value: "data-table-expand" }
+        ],
+        lessonDocumentationEdit: {},
+        selectedLessonPeriodDatetime: {},
+        recordedWeeks: [],
+      }
+    },
+    computed: {
+      emptyLessonPeriods() {
+        if (this.saveLessonDocumentationsPerWeek === "True") {
+          let currentDatetime = new Date()
+          let weeks = {}
+          let lpdts = this.plannedLessonPeriodsDateTimes.filter(lp => new Date(lp.datetimeStart) > currentDatetime)
+          for (let ldIndex in lpdts) {
+            let ld = lpdts[ldIndex]
+            if (ld.week in weeks) {
+              weeks[ld.week]["planned"].push(ld)
+            } else {
+              weeks[ld.week] = {
+                "year": ld.year,
+                "week": ld.week,
+                "startDate": this.calculateStartDateOfCW(ld.year, ld.week),
+                "datetimeStart": ld.datetimeStart,
+                "lessonPeriod": ld.lessonPeriod,
+                "planned": [ld]
+              }
+            }
+          }
+          return Object.values(weeks) // FIXME sort by date
+        } else {
+          let currentDatetime = new Date()
+          return this.plannedLessonPeriodsDateTimes.filter(lp => new Date(lp.datetimeStart) > currentDatetime)
+        }
+      },
+      computedLessonDocumentations() {
+        if (this.saveLessonDocumentationsPerWeek === "True") {
+          let weeks = {}
+          for (let ldIndex in this.lessonDocumentations) {
+            let ld = this.lessonDocumentations[ldIndex]
+            if (ld.week in weeks) {
+              weeks[ld.week]["documentations"].push(ld)
+            } else {
+              weeks[ld.week] = {
+                "id": ld.id,
+                "startDate": this.calculateStartDateOfCW(ld.year, ld.week),
+                "year": ld.year,
+                "week": ld.week,
+                "topic": ld.topic,
+                "homework": ld.homework,
+                "groupNote": ld.groupNote,
+                "documentations": [ld]
+              }
+            }
+          }
+          return Object.values(weeks)
+        } else {
+          return this.lessonDocumentations
+        }
+      }
+    },
+    methods: {
+      cancelDialog() {
+        this.dialog = false;
+        this.lessonDocumentationEdit = {};
+      },
+      recordDocumentation(item) {
+        if (this.recordedWeeks.includes(item.week)) {
+          return false
+        }
+        this.recordedWeeks.push(item.week)
+        return true
+      },
+      async loadLessonDocumentation(item) {
+        const result = await this.$apollo.mutate({
+          mutation: require("./LessonDocumentation.graphql"),
+          variables: {
+            year: item.year,
+            week: item.week,
+            lessonPeriodId: item.lessonPeriod ? item.lessonPeriod.id : null,
+            eventId: item.event ? item.event.id : null,
+            extraLessonId: item.extraLesson ? item.extraLesson.id : null,
+          },
+        })
+        let lessonDocumentation = result.data.updateOrCreateLessonDocumentation.lessonDocumentation
+        this.lessonDocumentationEdit = {
+          id: lessonDocumentation.id,
+          year: item.year,
+          week: item.week,
+          date: lessonDocumentation.date,
+          period: item.period,
+          lessonPeriodId: item.lessonPeriod ? item.lessonPeriod.id : null,
+          eventId: item.event ? item.event.id : null,
+          extraLessonId: item.extraLesson ? item.extraLesson.id : null,
+          topic: lessonDocumentation.topic,
+          homework: lessonDocumentation.homework,
+          groupNote: lessonDocumentation.groupNote,
+          personalNotes: lessonDocumentation.personalNotes,
+        }
+      },
+
+      editLessonDocumentation(item) {
+        if (this.saveLessonDocumentationsPerWeek === "True") {
+          this.loadLessonDocumentation(item.documentations[0])
+        } else {
+          this.loadLessonDocumentation(item)
+        }
+        this.dialog = true
+      },
+
+      createLessonDocumentation() { // FIXME: Update cache to show newly created LessonDocumentation in table
+        let lessonDocumentation = this.selectedLessonPeriodDatetime
+        lessonDocumentation["event"] = null
+        lessonDocumentation["extraLesson"] = null
+        this.loadLessonDocumentation(lessonDocumentation)
+        this.dialog = true
+      },
+
+      calculateStartDateOfCW(year, week){
+        let ld_date = new Date(Date.UTC(year, 0, 1 + (week - 1) * 7));
+        let dow = ld_date.getDay();
+        let start_date = ld_date;
+        if (dow <= 4)
+          return start_date.setDate(ld_date.getDate() - ld_date.getDay() + 1)
+        else
+          return start_date.setDate(ld_date.getDate() + 8 - ld_date.getDay())
+      },
+
+      getLessonText(item) {
+        let date_obj = new Date(item.hasOwnProperty("datetimeStart") ? item.datetimeStart : item.date)
+        let period = item.lessonPeriod ? ", " + this.$t('alsijil.period_number', {number: item.lessonPeriod.period.period}) : "" // FIXME: Cases without lessonPeriod
+        return this.$d(date_obj, "short") + period
+      },
+      getWeekText(item) {
+        if (item.hasOwnProperty("startDate")) {
+          var start_date = new Date(item.startDate)
+        } else {
+          let lesson_date = new Date(item.date)
+          var start_date = new Date(((lesson_date.getDay() || 7) !== 1) ? lesson_date.setHours(-24 * (lesson_date.getDay() - 1)) : lesson_date)
+        }
+        let end_date = new Date(start_date)
+        end_date.setDate(end_date.getDate() + 6)
+        return start_date.toLocaleDateString(this.$root.languageCode) + " - " + end_date.toLocaleDateString(this.$root.languageCode) + ", " + this.$root.django.gettext('CW') + " " + item.week
+      },
+      personalNoteString(personalNote) {
+          let personalNoteString = "";
+          if (personalNote.tardiness > 0) {
+              personalNoteString += personalNote.tardiness + " min. ";
+          }
+          if (personalNote.absent) {
+              personalNoteString += this.$t("absent") + ", ";
+          }
+          if (personalNote.excused) {
+              personalNoteString += this.$t("excused") + ", ";
+          }
+          if (personalNote.excuseType) {
+              personalNoteString += personalNote.excuseType.name;
+          }
+          if (personalNote.extraMarks.length > 0) {
+              personalNoteString += " (";
+              personalNote.extraMarks.forEach(item => {
+                  personalNoteString += item.name + ", ";
+              });
+              personalNoteString = personalNoteString.substring(0, personalNoteString.length - 2);
+              personalNoteString += ") ";
+          }
+          if (personalNote.remarks) {
+              personalNoteString += "\"" + personalNote.remarks + "\" ";
+          }
+          return personalNoteString;
+      },
+    }
+  }
+</script>
diff --git a/aleksis/apps/alsijil/assets/components/coursebook/PersonalNotes.vue b/aleksis/apps/alsijil/assets/components/coursebook/PersonalNotes.vue
new file mode 100644
index 0000000000000000000000000000000000000000..a7b8e3f5d304b32652793e90247a0bee28724725
--- /dev/null
+++ b/aleksis/apps/alsijil/assets/components/coursebook/PersonalNotes.vue
@@ -0,0 +1,359 @@
+<template>
+    <v-dialog
+      v-model="dialog"
+      max-width="600px"
+      @click:outside="cancelDialog"
+    >
+      <template v-slot:activator="{ on, attrs }">
+        <div>
+          <template v-for="personalNote in personalNotes">
+            <v-chip class="ma-1" close @click="editPersonalNote(personalNote.person.id)"
+              @click:close="removePersonalNote(personalNote.person.id)" v-if="personalNoteString(personalNote)">
+              {{ personalNote.person.fullName }}: {{ personalNoteString(personalNote) }}
+            </v-chip>
+          </template>
+        </div>
+        <v-tooltip bottom>
+          <template v-slot:activator="{ on, attrs }">
+            <v-btn
+              class="ma-1"
+              color="primary"
+              icon
+              outlined
+              v-bind="attrs"
+              v-on="on"
+              @click="createPersonalNote"
+            >
+              <v-icon>
+                mdi-plus
+              </v-icon>
+            </v-btn>
+          </template>
+          <span v-text="$t('alsijil.coursebook.add_personal_note')"></span>
+        </v-tooltip>
+      </template>
+      <v-card>
+        <v-card-title>
+          <span class="text-h5">Personal Note</span>
+        </v-card-title>
+        <v-card-text>
+          <v-container>
+            <v-select
+              item-text="fullName"
+              item-value="id"
+              :items="persons"
+              :label="$t('alsijil.personal_note.person')"
+              v-model="editedPersonID"
+              @input="updatePersonalNote"
+            ></v-select>
+            <v-text-field
+              :label="$t('alsijil.personal_note.tardiness')"
+              suffix="min" type="number"
+              min="0"
+              :disabled="editedPersonID === ID_NO_PERSON"
+              v-model="editedTardiness"
+            ></v-text-field>
+            <v-checkbox
+              :label="$t('alsijil.personal_note.absent')"
+              v-model="editedAbsent"
+              :disabled="editedPersonID === ID_NO_PERSON"
+              @change="editedExcused = false; editedExcuseType = null"
+            ></v-checkbox>
+            <v-checkbox
+              :label="$t('alsijil.personal_note.excused')"
+              v-model="editedExcused"
+              :disabled="editedPersonID === ID_NO_PERSON || !editedAbsent"
+              @change="editedExcuseType = null"
+            ></v-checkbox>
+            <v-select
+              :label="$t('alsijil.personal_note.excuse_type')"
+              v-model="editedExcuseType"
+              :items="excuseTypes"
+              item-text="name"
+              return-object
+              :disabled="editedPersonID === ID_NO_PERSON || !editedAbsent || !editedExcused"
+            ></v-select>
+            <v-select
+              :label="$t('alsijil.personal_note.extra_marks')"
+              v-model="editedExtraMarks"
+              :items="extraMarks"
+              item-text="name"
+              return-object
+              :disabled="editedPersonID === ID_NO_PERSON"
+              multiple
+              chips
+            ></v-select>
+            <v-text-field
+              :label="$t('alsijil.personal_note.remarks')"
+              v-model="editedRemarks"
+              :disabled="editedPersonID === ID_NO_PERSON"
+            ></v-text-field>
+          </v-container>
+        </v-card-text>
+        <v-card-actions>
+          <v-spacer></v-spacer>
+          <v-btn
+            color="error"
+            outlined
+            @click="cancelDialog"
+          >
+            {{ $t("alsijil.cancel") }}
+          </v-btn>
+          <v-btn
+            color="success"
+            @click="saveDialog"
+            :disabled="editedPersonID === ID_NO_PERSON"
+          >
+            {{ $t("alsijil.save") }}
+          </v-btn>
+        </v-card-actions>
+      </v-card>
+    </v-dialog>
+</template>
+
+<script>
+import gql from 'graphql-tag';
+
+const ID_NO_PERSON = null;
+
+export default {
+    model: {
+        prop: "personalNotes",
+        event: "change",
+    },
+    created() {
+        this.ID_NO_PERSON = ID_NO_PERSON;
+    },
+    methods: {
+        removePersonalNote(personID) {
+            if (personID === ID_NO_PERSON) {
+                return
+            }
+            console.log("removing personal note of person", personID);
+            this.editedPersonID = personID;
+            this.editedTardiness = 0;
+            this.editedAbsent = false;
+            this.editedExcused = false;
+            this.editedExcuseType = null;
+            this.editedExtraMarks = [];
+            this.editedRemarks = "";
+
+            this.savePersonalNote();
+        },
+        editPersonalNote(personID) {
+            console.log("editing personal note of person", personID);
+            this.editedPersonID = personID;
+            this.updatePersonalNote();
+            this.dialog = true;
+        },
+        updatePersonalNote() {
+            let personalNote = this.personalNoteByStudentID(this.editedPersonID);
+            this.editedTardiness = personalNote.tardiness || 0;
+            this.editedAbsent = personalNote.absent || false;
+            this.editedExcused = personalNote.excused || false;
+            this.editedExcuseType = personalNote.excuseType || null;
+            this.editedExtraMarks = personalNote.extraMarks || [];
+            this.editedRemarks = personalNote.remarks || "";
+
+            this.newPersonalNote = !!(personalNote && Object.keys(personalNote).length === 0 && Object.getPrototypeOf(personalNote) === Object.prototype);
+        },
+        createPersonalNote() {
+            this.editedPersonID = ID_NO_PERSON;
+            this.editedTardiness = 0;
+            this.editedAbsent = false;
+            this.editedExcused = false;
+            this.editedExcuseType = null;
+            this.editedExtraMarks = [];
+            this.editedRemarks = "";
+            this.newPersonalNote = true;
+            this.dialog = true;
+        },
+        personalNoteByStudentID(studentID) {
+            if (this.editedPersonID === ID_NO_PERSON) {
+                return {};
+            }
+            return this.personalNotes.filter(item => item.person.id === studentID)[0] || {};
+        },
+        savePersonalNote() {
+            if (this.editedPersonID === ID_NO_PERSON) {
+                return
+            }
+
+            let editedExcuseTypeID = (this.editedExcuseType) ? this.editedExcuseType.id : null;
+            let editedExtraMarksIDs = [];
+            this.editedExtraMarks.forEach(item => {editedExtraMarksIDs.push(item.id);});
+
+            // We save the user input in case of an error
+            const variables = {
+                "personId": this.editedPersonID,
+                "tardiness": this.editedTardiness,
+                "absent": this.editedAbsent,
+                "excused": this.editedExcused,
+                "excuseType": editedExcuseTypeID,
+                "extraMarks": editedExtraMarksIDs,
+                "remarks": this.editedRemarks,
+                "lessonDocumentation": this.lessonDocumentationId,
+            }
+
+            console.log(variables)
+
+            // Call to the graphql mutation
+            this.$apollo.mutate({
+                // Query
+                mutation: gql`mutation updateOrCreatePersonalNote(
+                    $personId: ID!,
+                    $lessonDocumentation: ID!,
+                    $tardiness: Int,
+                    $absent: Boolean,
+                    $excused: Boolean, 
+                    $excuseType: ID, 
+                    $extraMarks: [ID],
+                    $remarks: String
+                ) {
+                    updateOrCreatePersonalNote(personId: $personId,
+                        lessonDocumentation: $lessonDocumentation,
+                        tardiness: $tardiness,
+                        absent: $absent,
+                        excused: $excused,
+                        excuseType: $excuseType,
+                        extraMarks: $extraMarks,
+                        remarks: $remarks
+                    ) {
+                        personalNote {
+                            id
+                            person {
+                                id
+                                fullName
+                            }
+                            tardiness
+                            remarks
+                            absent
+                            excused
+                            excuseType {
+                                id
+                            }
+                            extraMarks {
+                                id
+                            }
+                        }
+                    }
+                }
+                `,
+                // Parameters
+                variables: variables,
+            }).then((data) => {
+                // Result
+                console.log(data)
+                // FIXME: check if data changed (?), display success message
+            }).catch((error) => {
+                // Error
+                console.error(error)
+                // FIXME: Notify the user about the error, maybe retry
+            })
+
+            if (this.newPersonalNote) {
+                this.personalNotes.push({
+                    person: {
+                        id: this.editedPersonID,
+                        fullName: this.studentNameByID(this.editedPersonID)
+                    },
+                    tardiness: this.editedTardiness,
+                    absent: this.editedAbsent,
+                    excused: this.editedExcused,
+                    excuseType: this.editedExcuseType,
+                    extraMarks: this.editedExtraMarks,
+                    remarks: this.editedRemarks,
+                });
+            } else {
+                // Loop through all personal notes and update the ones that match the editedPersonID
+                this.personalNotes.forEach(item => {
+                    if (item.person.id === this.editedPersonID) {
+                        item.tardiness = this.editedTardiness;
+                        item.absent = this.editedAbsent;
+                        item.excused = this.editedExcused;
+                        item.excuseType = this.editedExcuseType;
+                        item.extraMarks = this.editedExtraMarks;
+                        item.remarks = this.editedRemarks;
+                    }
+                });
+            }
+            this.$emit('change', this.personalNotes)
+        },
+        cancelDialog() {
+            this.dialog = false;
+            this.editedPersonID = ID_NO_PERSON;
+        },
+        saveDialog() {
+            this.savePersonalNote();
+            this.dialog = false;
+            this.editedPersonID = ID_NO_PERSON;
+        },
+        personalNoteString(personalNote) {
+            let personalNoteString = "";
+            if (personalNote.tardiness > 0) {
+                personalNoteString += personalNote.tardiness + " min. ";
+            }
+            if (personalNote.absent) {
+                personalNoteString += $t("alsijil.absent") + " ";
+            }
+            if (personalNote.excused) {
+                personalNoteString += $t("alsijil.excused") + " ";
+            }
+            if (personalNote.excuseType) {
+                personalNoteString += personalNote.excuseType.name;
+            }
+            if (personalNote.extraMarks.length > 0) {
+                personalNoteString += " (";
+                personalNote.extraMarks.forEach(item => {
+                    personalNoteString += item.name + ", ";
+                });
+                personalNoteString = personalNoteString.substring(0, personalNoteString.length - 2);
+                personalNoteString += ") ";
+            }
+            if (personalNote.remarks) {
+                personalNoteString += "\"" + personalNote.remarks + "\" ";
+            }
+            return personalNoteString;
+        },
+        studentNameByID(studentID) {
+            try {
+                return this.persons.filter(item => item.id === studentID)[0].fullName;
+            } catch (TypeError) {
+                return "";
+            }
+        }
+    },
+    props: ["lessonDocumentationId", "personalNotes", "groups", "excuseTypes", "extraMarks"],
+    name: "personal-notes",
+    data: () => {
+        return {
+            dialog: false,
+            // Absent versp. exc. type hw note
+            editPersonalNoteId: null,
+            editedPersonID: ID_NO_PERSON,
+            editedTardiness: 0,
+            editedAbsent: false,
+            editedExcused: false,
+            editedExcuseType: null,
+            editedExtraMarks: [],
+            editedRemarks: "",
+            newPersonalNote: false,
+        }
+    },
+    computed: {
+        persons() {
+            // go through each group and get the students
+            // use the group names as headers for the v-select
+
+            return this.groups.map(
+                group => {
+                    return [
+                        {header: group.name, id: group.shortName},
+                        group.members
+                    ]
+                }
+            ).flat(2);
+        }
+    }
+}
+</script>
diff --git a/aleksis/apps/alsijil/assets/components/coursebook/UpdateIndicator.vue b/aleksis/apps/alsijil/assets/components/coursebook/UpdateIndicator.vue
new file mode 100644
index 0000000000000000000000000000000000000000..4ae2d9518d0be459cf71b80df738cbb97a5fc617
--- /dev/null
+++ b/aleksis/apps/alsijil/assets/components/coursebook/UpdateIndicator.vue
@@ -0,0 +1,80 @@
+<template>
+  <v-tooltip bottom>
+    <template v-slot:activator="{ on, attrs }">
+      <v-btn
+        right
+        icon
+
+        v-bind="attrs"
+        v-on="on"
+
+        @click="() => {isAbleToClick ? $emit('manual-update') : null}"
+        :loading="status === UPDATING"
+      >
+        <v-icon
+          v-if="status !== UPDATING"
+          :color="color"
+        >
+          {{ icon }}
+        </v-icon>
+      </v-btn>
+    </template>
+    <span>{{ text }}</span>
+  </v-tooltip>
+</template>
+
+<script>
+import {CHANGES, ERROR, SAVED, UPDATING} from "../../UpdateStatuses.js";
+
+export default {
+    created() {
+        this.ERROR = ERROR;
+        this.SAVED = SAVED;
+        this.UPDATING = UPDATING;
+        this.CHANGES = CHANGES;
+    },
+    name: "update-indicator",
+    emits: ["manual-update"],
+    props: ["status"],
+    computed: {
+        text() {
+            switch (this.status) {
+                case SAVED:
+                    return this.$t("alsijil.coursebook.sync.saved");
+                case UPDATING:
+                    return this.$t("alsijil.coursebook.sync.updating");
+                case CHANGES:
+                    return this.$t("alsijil.coursebook.sync.changes");
+                default:
+                    return this.$t("alsijil.coursebook.sync.error");
+            }
+        },
+        color() {
+            switch (this.status) {
+                case SAVED:
+                    return "success";
+                case CHANGES:
+                    return "secondary";
+                case UPDATING:
+                    return "secondary";
+                default:
+                    return "error";
+            }
+        },
+        icon() {
+            // FIXME use app sdhasdhahsdhsadhsadh
+            switch (this.status) {
+                case SAVED:
+                    return "mdi-check-circle-outline";
+                case CHANGES:
+                    return "mdi-dots-horizontal";
+                default:
+                    return "mdi-alert-outline";
+            }
+        },
+        isAbleToClick() {
+            return this.status === CHANGES || this.status === ERROR;
+        }
+    },
+}
+</script>
diff --git a/aleksis/apps/alsijil/assets/index.js b/aleksis/apps/alsijil/assets/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..77d41966e5d3688accb0916b457b6be9ea390c73
--- /dev/null
+++ b/aleksis/apps/alsijil/assets/index.js
@@ -0,0 +1,3 @@
+export default [
+    { path: "/coursebook/:lessonId", component: () => import("./components/coursebook/CourseBook.vue"), props: true },
+];
diff --git a/aleksis/apps/alsijil/assets/messages.json b/aleksis/apps/alsijil/assets/messages.json
new file mode 100644
index 0000000000000000000000000000000000000000..ecf5f79e4ece66dbe7b7a35f21af2a773f944a44
--- /dev/null
+++ b/aleksis/apps/alsijil/assets/messages.json
@@ -0,0 +1,53 @@
+{
+  "en": {
+    "alsijil": {
+      "coursebook": {
+        "title": "Coursebook",
+        "create_documentation": "Create documentation",
+        "choose_week": "Choose week",
+        "choose_lesson_date": "Choose lesson date",
+        "sync": {
+          "saved": "All changes are saved.",
+          "updating": "Changes are being synced.",
+          "changes": "You have unsaved changes. Click to save them immediately.",
+          "error": "There has been an error while saving the latest changes."
+        }
+      },
+      "period": "Period",
+      "period_number": "{number}. period",
+      "lesson_documentation": {
+        "topic": "Topic",
+        "homework": "Homework",
+        "group_note": "Group note"
+      },
+      "calendar_week": "Calendar week",
+      "calendar_week_short": "Week",
+      "personal_note": {
+        "title": "Personal Note",
+        "title_plural": "Personal Notes",
+        "absent_title": "Absent",
+        "excused_title": "Excused",
+        "absent": "absent",
+        "excused": "excused",
+        "person": "Person",
+        "tardiness": "Tardiness",
+        "excuse_type": "Excuse type",
+        "extra_marks": "Extra marks",
+        "remarks": "Remarks",
+        "actions": {
+          "add": "Add personal note"
+        }
+      },
+      "error_occurred": "An error occurred",
+      "error_updating": "Error updating data",
+      "cancel": "Cancel",
+      "save": "Save",
+      "back": "Back"
+    }
+  },
+  "de": {
+    "coursebook": {
+      "title": "Kursbuch"
+    }
+  }
+}
diff --git a/aleksis/apps/alsijil/forms.py b/aleksis/apps/alsijil/forms.py
index 4ef9b4e24e7315c75cd6106b72f3853410e9ef7b..3c5630cbfc1653db99107159e1b8834507a27a6d 100644
--- a/aleksis/apps/alsijil/forms.py
+++ b/aleksis/apps/alsijil/forms.py
@@ -47,7 +47,7 @@ class LessonDocumentationForm(forms.ModelForm):
         self.fields["homework"].label = _("Homework for the next lesson")
         if (
             self.instance.lesson_period
-            and get_site_preferences()["alsijil__allow_carry_over_same_week"]
+            and get_site_preferences()["alsijil__save_lesson_documentations_by_week"]
         ):
             self.fields["carry_over_week"] = forms.BooleanField(
                 label=_("Carry over data to all other lessons with the same subject in this week"),
@@ -58,7 +58,7 @@ class LessonDocumentationForm(forms.ModelForm):
     def save(self, **kwargs):
         lesson_documentation = super(LessonDocumentationForm, self).save(commit=True)
         if (
-            get_site_preferences()["alsijil__allow_carry_over_same_week"]
+            get_site_preferences()["alsijil__save_lesson_documentations_by_week"]
             and self.cleaned_data["carry_over_week"]
             and (
                 lesson_documentation.topic
@@ -68,7 +68,7 @@ class LessonDocumentationForm(forms.ModelForm):
             and lesson_documentation.lesson_period
         ):
             lesson_documentation.carry_over_data(
-                LessonPeriod.objects.filter(lesson=lesson_documentation.lesson_period.lesson)
+                LessonPeriod.objects.filter(lesson=lesson_documentation.lesson_period.lesson), True
             )
 
 
diff --git a/aleksis/apps/alsijil/menus.py b/aleksis/apps/alsijil/menus.py
index fcf14e7cc8f8ea51eabee00abb5a75c61de42af8..bd351495f7e5aa4159f47ac50e5df40ed17698d1 100644
--- a/aleksis/apps/alsijil/menus.py
+++ b/aleksis/apps/alsijil/menus.py
@@ -6,16 +6,30 @@ MENUS = {
             "name": _("Class register"),
             "url": "#",
             "svg_icon": "mdi:book-open-outline",
+            "vuetify_icon": "mdi-book-open-outline",
             "root": True,
             "validators": [
                 "menu_generator.validators.is_authenticated",
                 "aleksis.core.util.core_helpers.has_person",
             ],
             "submenu": [
+                {
+                    "name": _("Coursebook"),
+                    "url": "select_coursebook",
+                    "svg_icon": "mdi:book-education-outline",
+                    "vuetify_icon": "mdi-book-education-outline",
+                    "validators": [
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "alsijil.view_coursebook_rule",
+                        ),
+                    ],
+                },
                 {
                     "name": _("Current lesson"),
                     "url": "lesson_period",
                     "svg_icon": "mdi:alarm",
+                    "vuetify_icon": "mdi-alarm",
                     "validators": [
                         (
                             "aleksis.core.util.predicates.permission_validator",
@@ -27,6 +41,7 @@ MENUS = {
                     "name": _("Current week"),
                     "url": "week_view",
                     "svg_icon": "mdi:view-week-outline",
+                    "vuetify_icon": "mdi-view-week-outline",
                     "validators": [
                         (
                             "aleksis.core.util.predicates.permission_validator",
@@ -38,6 +53,7 @@ MENUS = {
                     "name": _("My groups"),
                     "url": "my_groups",
                     "svg_icon": "mdi:account-multiple-outline",
+                    "vuetify_icon": "mdi-account-multiple-outline",
                     "validators": [
                         (
                             "aleksis.core.util.predicates.permission_validator",
@@ -49,6 +65,7 @@ MENUS = {
                     "name": _("My overview"),
                     "url": "overview_me",
                     "svg_icon": "mdi:chart-box-outline",
+                    "vuetify_icon": "mdi-chart-box-outline",
                     "validators": [
                         (
                             "aleksis.core.util.predicates.permission_validator",
@@ -60,6 +77,7 @@ MENUS = {
                     "name": _("My students"),
                     "url": "my_students",
                     "svg_icon": "mdi:account-school-outline",
+                    "vuetify_icon": "mdi-account-school-outline",
                     "validators": [
                         (
                             "aleksis.core.util.predicates.permission_validator",
@@ -71,6 +89,7 @@ MENUS = {
                     "name": _("Assign group role"),
                     "url": "assign_group_role_multiple",
                     "svg_icon": "mdi:clipboard-account-outline",
+                    "vuetify_icon": "mdi-clipboard-account-outline",
                     "validators": [
                         (
                             "aleksis.core.util.predicates.permission_validator",
@@ -82,6 +101,7 @@ MENUS = {
                     "name": _("All lessons"),
                     "url": "all_register_objects",
                     "svg_icon": "mdi:format-list-text",
+                    "vuetify_icon": "mdi-format-list-text",
                     "validators": [
                         (
                             "aleksis.core.util.predicates.permission_validator",
@@ -92,7 +112,8 @@ MENUS = {
                 {
                     "name": _("Register absence"),
                     "url": "register_absence",
-                    "icon": "rate_review",
+                    "svg_icon": "mdi:message-draw",
+                    "vuetify_icon": "mdi-message-draw",
                     "validators": [
                         (
                             "aleksis.core.util.predicates.permission_validator",
@@ -104,6 +125,7 @@ MENUS = {
                     "name": _("Excuse types"),
                     "url": "excuse_types",
                     "svg_icon": "mdi:label-outline",
+                    "vuetify_icon": "mdi-label-outline",
                     "validators": [
                         (
                             "aleksis.core.util.predicates.permission_validator",
@@ -115,6 +137,7 @@ MENUS = {
                     "name": _("Extra marks"),
                     "url": "extra_marks",
                     "svg_icon": "mdi:label-variant-outline",
+                    "vuetify_icon": "mdi-label-variant-outline",
                     "validators": [
                         (
                             "aleksis.core.util.predicates.permission_validator",
@@ -126,6 +149,7 @@ MENUS = {
                     "name": _("Manage group roles"),
                     "url": "group_roles",
                     "svg_icon": "mdi:clipboard-plus-outline",
+                    "vuetify_icon": "mdi-clipboard-plus-outline",
                     "validators": [
                         (
                             "aleksis.core.util.predicates.permission_validator",
diff --git a/aleksis/apps/alsijil/model_extensions.py b/aleksis/apps/alsijil/model_extensions.py
index 2c5420200e3dd9a6c60ad562e495d198be4a8b39..60504010d83ddcedb7a73646a28a2eb421e2fa62 100644
--- a/aleksis/apps/alsijil/model_extensions.py
+++ b/aleksis/apps/alsijil/model_extensions.py
@@ -461,7 +461,9 @@ def generate_person_list_with_class_register_statistics(
         ),
         tardiness=Sum("filtered_personal_notes__tardiness"),
         tardiness_count=Count(
-            "filtered_personal_notes", filter=Q(filtered_personal_notes__tardiness__gt=0), distinct=True
+            "filtered_personal_notes",
+            filter=Q(filtered_personal_notes__tardiness__gt=0),
+            distinct=True,
         ),
     )
 
diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py
index b504a294164a6388099148fda5149091767577a7..2cd7b24c64ef7314a47557158e7c8af09d1abf6f 100644
--- a/aleksis/apps/alsijil/models.py
+++ b/aleksis/apps/alsijil/models.py
@@ -347,11 +347,13 @@ class LessonDocumentation(RegisterObjectRelatedMixin, ExtensibleModel):
     homework = models.CharField(verbose_name=_("Homework"), max_length=200, blank=True)
     group_note = models.CharField(verbose_name=_("Group note"), max_length=200, blank=True)
 
-    def carry_over_data(self, all_periods_of_lesson: LessonPeriod):
-        """Carry over data to given periods in this lesson if data is not already set.
+    def carry_over_data(self, all_periods_of_lesson: LessonPeriod, force: bool):
+        """Carry over data to given periods in this lesson.
+
+        Does overwrite existing data in case ``force`` is set to ``True``.
 
         Both forms of carrying over data can be deactivated using site preferences
-        ``alsijil__carry_over_next_periods`` and ``alsijil__allow_carry_over_same_week``
+        ``alsijil__carry_over_next_periods`` and ``alsijil__save_lesson_documentations_by_week``
         respectively.
         """
         for period in all_periods_of_lesson:
@@ -361,15 +363,15 @@ class LessonDocumentation(RegisterObjectRelatedMixin, ExtensibleModel):
 
             changed = False
 
-            if not lesson_documentation.topic:
+            if not lesson_documentation.topic or force:
                 lesson_documentation.topic = self.topic
                 changed = True
 
-            if not lesson_documentation.homework:
+            if not lesson_documentation.homework or force:
                 lesson_documentation.homework = self.homework
                 changed = True
 
-            if not lesson_documentation.group_note:
+            if not lesson_documentation.group_note or force:
                 lesson_documentation.group_note = self.group_note
                 changed = True
 
@@ -390,7 +392,8 @@ class LessonDocumentation(RegisterObjectRelatedMixin, ExtensibleModel):
                 LessonPeriod.objects.filter(
                     lesson=self.lesson_period.lesson,
                     period__weekday=self.lesson_period.period.weekday,
-                )
+                ),
+                False,
             )
         super().save(*args, **kwargs)
 
diff --git a/aleksis/apps/alsijil/preferences.py b/aleksis/apps/alsijil/preferences.py
index 2fd34fa7fc4f802a52ead1d2d0291789be4fb9ce..014435f1b3072ab8804e2e57d1fdb6c7d135bca7 100644
--- a/aleksis/apps/alsijil/preferences.py
+++ b/aleksis/apps/alsijil/preferences.py
@@ -67,17 +67,11 @@ class CarryOverDataToNextPeriods(BooleanPreference):
 
 
 @site_preferences_registry.register
-class AllowCarryOverLessonDocumentationToCurrentWeek(BooleanPreference):
+class SaveLessonDocumentationsPerWeek(BooleanPreference):
     section = alsijil
-    name = "allow_carry_over_same_week"
+    name = "save_lesson_documentations_by_week"
     default = False
-    verbose_name = _(
-        "Allow carrying over data from any lesson period to all other lesson \
-                periods with the same lesson and in the same week"
-    )
-    help_text = _(
-        "This will carry over data only if the data in the aforementioned periods are empty."
-    )
+    verbose_name = _("Save lesson documentations per week instead of per lesson period")
 
 
 @site_preferences_registry.register
diff --git a/aleksis/apps/alsijil/rules.py b/aleksis/apps/alsijil/rules.py
index e9011c6c2b33ed58a21b6412f6c7e366b1b5e49e..83af497c900ea9ed54df5d3f8e4f7b63828d2ebe 100644
--- a/aleksis/apps/alsijil/rules.py
+++ b/aleksis/apps/alsijil/rules.py
@@ -204,6 +204,11 @@ view_students_list_predicate = view_my_groups_predicate & (
 )
 add_perm("alsijil.view_students_list_rule", view_students_list_predicate)
 
+# View CourseBook
+view_coursebook_predicate = has_person & is_teacher
+add_perm("alsijil.view_coursebook_rule", view_my_students_predicate)
+
+
 # View person overview
 view_person_overview_predicate = has_person & (
     (is_current_person & is_site_preference_set("alsijil", "view_own_personal_notes"))
diff --git a/aleksis/apps/alsijil/schema.py b/aleksis/apps/alsijil/schema.py
new file mode 100644
index 0000000000000000000000000000000000000000..d7f746170a13985dd7f5b012f71017dfb50c303e
--- /dev/null
+++ b/aleksis/apps/alsijil/schema.py
@@ -0,0 +1,249 @@
+from datetime import datetime
+
+import graphene
+from graphene_django import DjangoObjectType
+
+from aleksis.apps.chronos.models import Lesson
+from aleksis.core.models import Group, Person
+from aleksis.core.util.core_helpers import get_site_preferences
+
+from .models import (
+    Event,
+    ExcuseType,
+    ExtraLesson,
+    ExtraMark,
+    LessonDocumentation,
+    LessonPeriod,
+    PersonalNote,
+)
+
+
+class ExcuseTypeType(DjangoObjectType):
+    class Meta:
+        model = ExcuseType
+
+
+class PersonalNoteType(DjangoObjectType):
+    class Meta:
+        model = PersonalNote
+
+
+class LessonDocumentationType(DjangoObjectType):
+    class Meta:
+        model = LessonDocumentation
+
+    personal_notes = graphene.List(PersonalNoteType)
+    date = graphene.Field(graphene.Date)
+    period = graphene.Field(graphene.Int)
+
+    def resolve_personal_notes(root: LessonDocumentation, info, **kwargs):
+        persons = Person.objects.filter(
+            member_of__in=Group.objects.filter(pk__in=root.register_object.get_groups().all())
+        )
+        return PersonalNote.objects.filter(
+            week=root.week,
+            year=root.year,
+            lesson_period=root.lesson_period,
+            person__in=persons,
+        )
+
+    def resolve_period(root: LessonDocumentation, info, **kwargs):
+        return root.period.period
+
+    def resolve_date(root: LessonDocumentation, info, **kwargs):
+        return root.date
+
+
+class ExtraMarkType(DjangoObjectType):
+    class Meta:
+        model = ExtraMark
+
+
+class LessonDocumentationMutation(graphene.Mutation):
+    class Arguments:
+        year = graphene.Int(required=True)
+        week = graphene.Int(required=True)
+
+        lesson_period_id = graphene.ID(required=False)
+        event_id = graphene.ID(required=False)
+        extra_lesson_id = graphene.ID(required=False)
+
+        lesson_documentation_id = graphene.ID(required=False)
+
+        topic = graphene.String(required=False)
+        homework = graphene.String(required=False)
+        group_note = graphene.String(required=False)
+
+    lesson_documentation = graphene.Field(LessonDocumentationType)
+
+    @classmethod
+    def mutate(
+        cls,
+        root,
+        info,
+        year,
+        week,
+        lesson_period_id=None,
+        event_id=None,
+        extra_lesson_id=None,
+        lesson_documentation_id=None,
+        topic=None,
+        homework=None,
+        group_note=None,
+    ):
+
+        lesson_period = LessonPeriod.objects.filter(pk=lesson_period_id).first()
+        event = Event.objects.filter(pk=event_id).first()
+        extra_lesson = ExtraLesson.objects.filter(pk=extra_lesson_id).first()
+
+        lesson_documentation, created = LessonDocumentation.objects.get_or_create(
+            year=year,
+            week=week,
+            lesson_period=lesson_period,
+            event=event,
+            extra_lesson=extra_lesson,
+        )
+
+        if topic is not None:
+            lesson_documentation.topic = topic
+        if homework is not None:
+            lesson_documentation.homework = homework
+        if group_note is not None:
+            lesson_documentation.group_note = group_note
+
+        lesson_documentation.save()
+
+        if (
+            get_site_preferences()["alsijil__save_lesson_documentations_by_week"]
+            and (
+                lesson_documentation.topic
+                or lesson_documentation.homework
+                or lesson_documentation.group_note
+            )
+            and lesson_documentation.lesson_period
+        ):
+            lesson_documentation.carry_over_data(
+                LessonPeriod.objects.filter(lesson=lesson_documentation.lesson_period.lesson), True
+            )
+
+        return LessonDocumentationMutation(lesson_documentation=lesson_documentation)
+
+
+class PersonalNoteMutation(graphene.Mutation):
+    class Arguments:
+        person_id = graphene.ID(required=True)
+        lesson_documentation = graphene.ID(required=True)
+
+        personal_note_id = graphene.ID(required=False)  # Update or create personal note
+
+        late = graphene.Int(required=False)
+        absent = graphene.Boolean(required=False)
+        excused = graphene.Boolean(required=False)
+        excuse_type = graphene.ID(required=False)
+        remarks = graphene.String(required=False)
+        extra_marks = graphene.List(graphene.ID, required=False)
+
+    personal_note = graphene.Field(PersonalNoteType)
+
+    @classmethod
+    def mutate(
+        cls,
+        root,
+        info,
+        person_id,
+        lesson_documentation,
+        personal_note_id=None,
+        late=None,
+        absent=None,
+        excused=None,
+        excuse_type=None,
+        remarks=None,
+        extra_marks=None,
+    ):
+        person = Person.objects.get(pk=person_id)
+        lesson_documentation = LessonDocumentation.objects.get(pk=lesson_documentation)
+
+        personal_note, created = PersonalNote.objects.get_or_create(
+            person=person,
+            event=lesson_documentation.event,
+            extra_lesson=lesson_documentation.extra_lesson,
+            lesson_period=lesson_documentation.lesson_period,
+            week=lesson_documentation.week,
+            year=lesson_documentation.year,
+        )
+        if late is not None:
+            personal_note.late = late
+        if absent is not None:
+            personal_note.absent = absent
+        if excused is not None:
+            personal_note.excused = excused
+        if excuse_type is not None:
+            personal_note.excuse_type = ExcuseType.objects.get(pk=excuse_type)
+        if remarks is not None:
+            personal_note.remarks = remarks
+
+        if created:
+            personal_note.groups_of_person.set(person.member_of.all())
+
+        personal_note.save()
+
+        if extra_marks is not None:
+            extra_marks = ExtraMark.objects.filter(pk__in=extra_marks)
+            personal_note.extra_marks.set(extra_marks)
+            personal_note.save()
+        return PersonalNoteMutation(personal_note=personal_note)
+
+
+class Mutation(graphene.ObjectType):
+    update_or_create_lesson_documentation = LessonDocumentationMutation.Field()
+    update_or_create_personal_note = PersonalNoteMutation.Field()
+    # update_personal_note = PersonalNoteMutation.Field()
+
+
+class Query(graphene.ObjectType):
+    excuse_types = graphene.List(ExcuseTypeType)
+    lesson_documentations = graphene.List(LessonDocumentationType)
+    lesson_documentation_by_id = graphene.Field(LessonDocumentationType, id=graphene.ID())
+    lesson_documentations_by_lesson_id = graphene.List(LessonDocumentationType, id=graphene.ID())
+    personal_notes = graphene.List(PersonalNoteType)
+    extra_marks = graphene.List(ExtraMarkType)
+
+    def resolve_excuse_types(root, info, **kwargs):
+        # FIXME do permission stuff
+        return ExcuseType.objects.all()
+
+    def resolve_lesson_documentations(root, info, **kwargs):
+        # FIXME do permission stuff
+        return LessonDocumentation.objects.all().order_by(
+            "-year", "-week", "-lesson_period__period__weekday", "-lesson_period__period__period"
+        )
+
+    def resolve_lesson_documentation_by_id(root, info, id, **kwargs):  # noqa
+        return LessonDocumentation.objects.get(id=id)
+
+    def resolve_lesson_documentations_by_lesson_id(root, info, id, **kwargs):  # noqa
+        lesson = Lesson.objects.get(id=id)
+        now = datetime.now()
+        for equal_lesson in lesson._equal_lessons:
+            for planned in equal_lesson.planned_lessonperiods_datetimes:
+                if planned["datetime_start"] <= now:
+                    LessonDocumentation.objects.get_or_create(
+                        week=planned["week"],
+                        year=planned["year"],
+                        lesson_period=planned["lesson_period"],
+                    )  # FIXME: Queries shouldn't alter data
+
+        return LessonDocumentation.objects.filter(
+            lesson_period_id__in=LessonPeriod.objects.filter(
+                lesson__in=lesson._equal_lessons
+            ).values_list("id", flat=True)
+        ).order_by(
+            "-year", "-week", "-lesson_period__period__weekday", "-lesson_period__period__period"
+        )
+
+    def resolve_personal_notes(root, info, **kwargs):
+        # FIXME do permission stuff
+        return PersonalNote.objects.all()
+
+    def resolve_extra_marks(root, info, **kwargs):
+        return ExtraMark.objects.all()
diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/coursebook.html b/aleksis/apps/alsijil/templates/alsijil/class_register/coursebook.html
new file mode 100644
index 0000000000000000000000000000000000000000..80378c66a06cbac1951cbf38f1d6d49bea896b49
--- /dev/null
+++ b/aleksis/apps/alsijil/templates/alsijil/class_register/coursebook.html
@@ -0,0 +1,16 @@
+{% extends "core/vue_base.html" %}
+{% load static i18n %}
+{% load render_bundle from webpack_loader %}
+
+{% block page_title %}
+  {% trans "Coursebook" %}
+{% endblock %}
+{% block browser_title %}{% trans "Coursebook" %} {{ lesson }}{% endblock %}
+{% block content %}
+<div class="text-h5">{{ lesson }}</div>
+<router-view save-lesson-documentations-per-week={{ SITE_PREFERENCES.alsijil__save_lesson_documentations_by_week }} />
+{% endblock %}
+
+{% block extra_body %}
+  {% render_bundle "aleksis.apps.alsijil" %}
+{% endblock %}
diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/select_coursebook.html b/aleksis/apps/alsijil/templates/alsijil/class_register/select_coursebook.html
new file mode 100644
index 0000000000000000000000000000000000000000..57114df23ebd2e1631f56596fd89c220740fdad8
--- /dev/null
+++ b/aleksis/apps/alsijil/templates/alsijil/class_register/select_coursebook.html
@@ -0,0 +1,53 @@
+{% extends "core/vue_base.html" %}
+{% load static i18n %}
+{% load render_bundle from webpack_loader %}
+
+{% block page_title %}{% trans "Select Coursebook" %}{% endblock %}
+{% block browser_title %}{% trans "Select Coursebook" %}{% endblock %}
+{% block content %}
+  <v-row>
+    {% for lesson in lessons %}
+      <v-col xs="12" sm="6" md="6" lg="4" xl="3" class="d-flex">
+        <v-card class="flex-grow-1">
+          <v-card-title>
+            {% for group in lesson.groups.all %}{{ group.short_name }}{% if not forloop.last %},{% endif %}{% endfor %}
+            · {{ lesson.subject.name }}
+          </v-card-title>
+          <v-card-subtitle>
+            {{ lesson.validity.date_start }}-{{ lesson.validity.date_end }}
+          </v-card-subtitle>
+          <v-card-text>
+            {{ lesson.teachers.all|join:"," }}
+          </v-card-text>
+{#          <v-spacer></v-spacer>#}
+          <v-card-actions>
+            <v-btn :href="urls.coursebook({{ lesson.pk }})" text color="secondary">
+              <v-icon left>mdi-book-search-outline</v-icon>
+              {% trans "Open in coursebook" %}
+            </v-btn>
+          </v-card-actions>
+        </v-card>
+      </v-col>
+    {% empty %}
+      <v-container
+        class="text-center fill-height"
+        style="height: calc(100vh - 58px);"
+      >
+        <v-row align="center">
+          <v-col>
+            <h1 class="text-h3 primary--text">
+              <v-icon color="error" x-large>mdi-book-off-outline</v-icon>
+              {% trans "No Coursebook" %}
+            </h1>
+
+            <p>{% trans "There are no courses where you are a teacher." %} </p>
+          </v-col>
+        </v-row>
+      </v-container>
+    {% endfor %}
+  </v-row>
+{% endblock %}
+
+{% block extra_body %}
+  {% render_bundle "aleksis.apps.alsijil" %}
+{% endblock %}
diff --git a/aleksis/apps/alsijil/urls.py b/aleksis/apps/alsijil/urls.py
index b06aebd352774727bb6bddca9dec8a0e28846a69..2cbc63fbbbe9932e3f7fb6e312980971a1fc20dc 100644
--- a/aleksis/apps/alsijil/urls.py
+++ b/aleksis/apps/alsijil/urls.py
@@ -3,6 +3,8 @@ from django.urls import path
 from . import views
 
 urlpatterns = [
+    path("coursebook/", views.SelectCoursebookView.as_view(), name="select_coursebook"),
+    path("coursebook/<int:pk>/", views.CoursebookView.as_view(), name="coursebook"),
     path("lesson", views.register_object, {"model": "lesson"}, name="lesson_period"),
     path(
         "lesson/<int:year>/<int:week>/<int:id_>",
diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py
index 487a27d3b51d1ced83275403e1537a84a7925b4f..55fc21792f9500fc7d9de0c92dc0d38244a4337f 100644
--- a/aleksis/apps/alsijil/views.py
+++ b/aleksis/apps/alsijil/views.py
@@ -17,7 +17,7 @@ from django.utils.http import url_has_allowed_host_and_scheme
 from django.utils.translation import gettext as _
 from django.views import View
 from django.views.decorators.cache import never_cache
-from django.views.generic import DetailView
+from django.views.generic import DetailView, TemplateView
 
 import reversion
 from calendarweek import CalendarWeek
@@ -28,9 +28,20 @@ from reversion.views import RevisionMixin
 from rules.contrib.views import PermissionRequiredMixin, permission_required
 
 from aleksis.apps.chronos.managers import TimetableType
-from aleksis.apps.chronos.models import Event, ExtraLesson, Holiday, LessonPeriod, TimePeriod
+from aleksis.apps.chronos.models import (
+    Event,
+    ExtraLesson,
+    Holiday,
+    Lesson,
+    LessonPeriod,
+    TimePeriod,
+)
 from aleksis.apps.chronos.util.build import build_weekdays
-from aleksis.apps.chronos.util.date import get_weeks_for_year, week_weekday_to_date
+from aleksis.apps.chronos.util.date import (
+    get_current_year,
+    get_weeks_for_year,
+    week_weekday_to_date,
+)
 from aleksis.core.mixins import (
     AdvancedCreateView,
     AdvancedDeleteView,
@@ -1349,3 +1360,54 @@ class AllRegisterObjectsView(PermissionRequiredMixin, View):
         if self.action_form.is_valid():
             self.action_form.execute()
         return render(request, "alsijil/class_register/all_objects.html", context)
+
+
+class CoursebookView(PermissionRequiredMixin, DetailView):
+    model = Lesson
+    template_name = "alsijil/class_register/coursebook.html"
+    permission_required = "alsijil.view_coursebook_rule"
+
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        return context
+
+
+class SelectCoursebookView(PermissionRequiredMixin, TemplateView):
+    template_name = "alsijil/class_register/select_coursebook.html"
+    permission_required = "alsijil.view_coursebook_rule"  # FIXME
+
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        person = self.request.user.person
+
+        current_week = CalendarWeek.current_week()
+        current_year = get_current_year()
+        # Show all future and the ones of last week
+
+        last_week, last_week_year = (
+            (current_week - 1, current_year)
+            if current_week >= 2
+            else (CalendarWeek.get_last_week_of_year(current_year - 1).week, current_year - 1)
+        )
+
+        last_week_query = Q(
+            lesson_periods__substitutions__week=last_week,
+            lesson_periods__substitutions__year=last_week_year,
+        )
+        this_week_query = Q(
+            lesson_periods__substitutions__week__gte=current_week,
+            lesson_periods__substitutions__year=current_year,
+        )
+        next_year_query = Q(lesson_periods__substitutions__year__gt=current_year)
+        context["lessons"] = (
+            Lesson.objects.filter(
+                Q(teachers=person)
+                | (
+                    Q(lesson_periods__substitutions__teachers=person)
+                    & (last_week_query | this_week_query | next_year_query)
+                )
+            )
+            .for_current_or_all()
+            .distinct()
+        )
+        return context
diff --git a/docs/conf.py b/docs/conf.py
index 59ca213449c1f46b86ef5340aaac7c21447cb4e8..e4b22f267c295959eede8facc6a4d3a2cf26f9d2 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -29,9 +29,9 @@ copyright = "2019-2022 The AlekSIS team"
 author = "The AlekSIS Team"
 
 # The short X.Y version
-version = "2.1"
+version = "3.0"
 # The full version, including alpha/beta/rc tags
-release = "2.2.dev0"
+release = "3.0.dev1"
 
 
 # -- General configuration ---------------------------------------------------
diff --git a/pyproject.toml b/pyproject.toml
index a3a4aea8d9b8f3cfd286cab366900bcce9ee5b9c..70cac0640e8dfb2b045ae566d77fbf23d78d5f0f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "AlekSIS-App-Alsijil"
-version = "2.2.dev0"
+version = "3.0.dev1"
 packages = [
     { include = "aleksis" }
 ]
@@ -22,7 +22,8 @@ authors = [
     "Hangzhi Yu <yuha@katharineum.de>",
     "Lloyd Meins <meinsll@katharineum.de>",
     "mirabilos <thorsten.glaser@teckids.org>",
-    "Tom Teichler <tom.teichler@teckids.org>"
+    "Tom Teichler <tom.teichler@teckids.org>",
+    "magicfelix <felix@felix-zauberer.de>"
 ]
 maintainers = [
     "Dominik George <dominik.george@teckids.org>",
@@ -48,9 +49,9 @@ secondary = true
 
 [tool.poetry.dependencies]
 python = "^3.9"
-aleksis-core = "^2.12"
-aleksis-app-chronos = "^2.2"
-aleksis-app-stoelindeling = { version = "^1.0", optional = true }
+aleksis-core = "^2.12.2"
+aleksis-app-chronos = "^3.0.0.dev0"
+aleksis-app-stoelindeling = { version = "^2.0.0.dev0", optional = true }
 
 [tool.poetry.dev-dependencies]
 aleksis-builddeps = "*"