diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue b/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue
index 7c1b9ac900da53ff7ab97a5b459e9be70891521f..b9e723c2926d6cf5ae8a532bc5a70c615ccbd5f7 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue
@@ -1,6 +1,6 @@
 <template>
   <div>
-    <c-r-u-d-iterator
+    <infinite-scrolling-date-sorted-c-r-u-d-iterator
       i18n-key="alsijil.coursebook"
       :gql-query="gqlQuery"
       :gql-additional-query-args="gqlQueryArgs"
@@ -15,85 +15,90 @@
       use-deep-search
     >
       <template #additionalActions="{ attrs, on }">
-        <coursebook-filters v-model="filters" />
+        <coursebook-filters :page-type="pageType" v-model="filters" />
+        <v-expand-transition>
+          <v-card
+            outlined
+            class="full-width"
+            v-show="
+              pageType === 'absences' && combinedSelectedParticipations.length
+            "
+          >
+            <v-card-text>
+              <v-row align="center">
+                <v-col cols="6">
+                  {{
+                    $tc(
+                      "alsijil.coursebook.absences.action_for_selected",
+                      combinedSelectedParticipations.length,
+                    )
+                  }}
+                </v-col>
+                <v-col cols="6">
+                  <absence-reason-buttons
+                    allow-empty
+                    empty-value="present"
+                    @input="handleMultipleAction"
+                  />
+                </v-col>
+              </v-row>
+            </v-card-text>
+          </v-card>
+        </v-expand-transition>
       </template>
-      <template #default="{ items }">
-        <coursebook-loader />
-        <coursebook-day
-          v-for="{ date, docs, first, last } in groupDocsByDay(items)"
-          v-intersect="{
-            handler: intersectHandler(date, first, last),
-            options: {
-              rootMargin: '-' + topMargin + 'px 0px 0px 0px',
-              threshold: [0, 1],
-            },
-          }"
-          :date="date"
-          :docs="docs"
-          :lastQuery="lastQuery"
-          :focus-on-mount="initDate && initDate.toMillis() === date.toMillis()"
-          @init="transition"
-          :key="'day-' + date"
-          ref="days"
-          :extra-marks="extraMarks"
-        />
-        <coursebook-loader />
 
-        <date-select-footer
-          :value="currentDate"
-          @input="gotoDate"
-          @prev="gotoPrev"
-          @next="gotoNext"
+      <template #item="{ item, lastQuery }">
+        <component
+          :is="itemComponent"
+          :extraMarks="extraMarks"
+          :documentation="item"
+          :affectedQuery="lastQuery"
+          :value="(selectedParticipations[item.id] ??= [])"
+          @input="selectParticipation(item.id, $event)"
         />
       </template>
+
       <template #loading>
         <coursebook-loader :number-of-days="10" :number-of-docs="5" />
       </template>
 
-      <template #no-data>
-        <CoursebookEmptyMessage icon="mdi-book-off-outline">
-          {{ $t("alsijil.coursebook.no_data") }}
-        </CoursebookEmptyMessage>
+      <template #itemLoader>
+        <DocumentationLoader />
       </template>
-
-      <template #no-results>
-        <CoursebookEmptyMessage icon="mdi-book-alert-outline">
-          {{
-            $t("alsijil.coursebook.no_results", {
-              search: $refs.iterator.search,
-            })
-          }}
-        </CoursebookEmptyMessage>
-      </template>
-    </c-r-u-d-iterator>
-    <absence-creation-dialog />
+    </infinite-scrolling-date-sorted-c-r-u-d-iterator>
+    <v-scale-transition>
+      <absence-creation-dialog v-if="pageType === 'absences'" />
+    </v-scale-transition>
   </div>
 </template>
 
 <script>
-import CRUDIterator from "aleksis.core/components/generic/CRUDIterator.vue";
-import DateSelectFooter from "aleksis.core/components/generic/DateSelectFooter.vue";
-import CoursebookDay from "./CoursebookDay.vue";
+import InfiniteScrollingDateSortedCRUDIterator from "aleksis.core/components/generic/InfiniteScrollingDateSortedCRUDIterator.vue";
 import { DateTime, Interval } from "luxon";
 import { documentationsForCoursebook } from "./coursebook.graphql";
+import AbsenceReasonButtons from "aleksis.apps.kolego/components/AbsenceReasonButtons.vue";
 import CoursebookFilters from "./CoursebookFilters.vue";
 import CoursebookLoader from "./CoursebookLoader.vue";
-import CoursebookEmptyMessage from "./CoursebookEmptyMessage.vue";
-import { extraMarks } from "../extra_marks/extra_marks.graphql";
-
+import DocumentationModal from "./documentation/DocumentationModal.vue";
+import DocumentationAbsencesModal from "./absences/DocumentationAbsencesModal.vue";
 import AbsenceCreationDialog from "./absences/AbsenceCreationDialog.vue";
+import { extraMarks } from "../extra_marks/extra_marks.graphql";
+import DocumentationLoader from "./documentation/DocumentationLoader.vue";
+import sendToServerMixin from "./absences/sendToServerMixin";
 
 export default {
   name: "Coursebook",
   components: {
-    CoursebookEmptyMessage,
+    DocumentationLoader,
+    AbsenceReasonButtons,
     CoursebookFilters,
     CoursebookLoader,
-    CRUDIterator,
-    DateSelectFooter,
-    CoursebookDay,
+    DocumentationModal,
+    DocumentationAbsencesModal,
+    InfiniteScrollingDateSortedCRUDIterator,
     AbsenceCreationDialog,
   },
+  mixins: [sendToServerMixin],
   props: {
     filterType: {
       type: String,
@@ -109,6 +114,11 @@ export default {
       required: false,
       default: null,
     },
+    pageType: {
+      type: String,
+      required: false,
+      default: "documentations",
+    },
     /**
      * Number of consecutive to load at once
      * This number of days is initially loaded and loaded
@@ -138,11 +148,13 @@ export default {
       groups: [],
       courses: [],
       incomplete: false,
+      absencesExist: true,
       ready: false,
       initDate: false,
       currentDate: "",
       hashUpdater: false,
       extraMarks: [],
+      selectedParticipations: {},
     };
   },
   apollo: {
@@ -162,6 +174,7 @@ export default {
         dateStart: this.dateStart,
         dateEnd: this.dateEnd,
         incomplete: !!this.incomplete,
+        absencesExist: !!this.absencesExist && this.pageType === "absences",
       };
     },
     filters: {
@@ -171,15 +184,20 @@ export default {
           objId: this.objId,
           filterType: this.filterType,
           incomplete: this.incomplete,
+          pageType: this.pageType,
+          absencesExist: this.absencesExist,
         };
       },
       set(selectedFilters) {
         if (Object.hasOwn(selectedFilters, "incomplete")) {
           this.incomplete = selectedFilters.incomplete;
+        } else if (Object.hasOwn(selectedFilters, "absencesExist")) {
+          this.absencesExist = selectedFilters.absencesExist;
         } else if (
           Object.hasOwn(selectedFilters, "filterType") ||
           Object.hasOwn(selectedFilters, "objId") ||
-          Object.hasOwn(selectedFilters, "objType")
+          Object.hasOwn(selectedFilters, "objType") ||
+          Object.hasOwn(selectedFilters, "pageType")
         ) {
           this.$router.push({
             name: "alsijil.coursebook",
@@ -189,194 +207,65 @@ export default {
                 : this.filterType,
               objType: selectedFilters.objType,
               objId: selectedFilters.objId,
+              pageType: selectedFilters.pageType
+                ? selectedFilters.pageType
+                : this.pageType,
             },
             hash: this.$route.hash,
           });
           // computed should not have side effects
           // but this was actually done before filters was refactored into
           // its own component
-          this.resetDate();
+          this.$refs.iterator.resetDate();
           // might skip query until both set = atomic
-        }
-      },
-    },
-  },
-  methods: {
-    resetDate(toDate) {
-      // Assure current date
-      console.log("Resetting date range", this.$route.hash);
-      this.currentDate = toDate || this.$route.hash?.substring(1);
-      if (!this.currentDate) {
-        console.log("Set default date");
-        this.setDate(DateTime.now().toISODate());
-      }
-
-      const date = DateTime.fromISO(this.currentDate);
-      this.initDate = date;
-      this.dateStart = date.minus({ days: this.dayIncrement }).toISODate();
-      this.dateEnd = date.plus({ days: this.dayIncrement }).toISODate();
-    },
-    transition() {
-      this.initDate = false;
-      this.ready = true;
-    },
-    groupDocsByDay(docs) {
-      // => {dt: {date: dt, docs: doc ...} ...}
-      const docsByDay = docs.reduce((byDay, doc) => {
-        // This works with dummy. Does actual doc have dateStart instead?
-        const day = DateTime.fromISO(doc.datetimeStart).startOf("day");
-        byDay[day] ??= { date: day, docs: [] };
-        byDay[day].docs.push(doc);
-        return byDay;
-      }, {});
-      // => [{date: dt, docs: doc ..., idx: idx, lastIdx: last-idx} ...]
-      // sorting is necessary since backend can send docs unordered
-      return Object.keys(docsByDay)
-        .sort()
-        .map((key, idx, { length }) => {
-          const day = docsByDay[key];
-          day.first = idx === 0;
-          const lastIdx = length - 1;
-          day.last = idx === lastIdx;
-          return day;
-        });
-    },
-    // docsByDay: {dt: [dt doc ...] ...}
-    fetchMore(from, to, then) {
-      console.log("fetching", from, to);
-      this.lastQuery.fetchMore({
-        variables: {
-          dateStart: from,
-          dateEnd: to,
-        },
-        // Transform the previous result with new data
-        updateQuery: (previousResult, { fetchMoreResult }) => {
-          console.log("Received more");
-          then();
-          return { items: previousResult.items.concat(fetchMoreResult.items) };
-        },
-      });
-    },
-    setDate(date) {
-      this.currentDate = date;
-      if (!this.hashUpdater) {
-        this.hashUpdater = window.requestIdleCallback(() => {
-          if (!(this.$route.hash.substring(1) === this.currentDate)) {
-            this.$router.replace({ hash: this.currentDate });
-          }
-          this.hashUpdater = false;
-        });
-      }
-    },
-    fixScrollPos(height, top) {
-      this.$nextTick(() => {
-        if (height < document.documentElement.scrollHeight) {
-          document.documentElement.scrollTop =
-            document.documentElement.scrollHeight - height + top;
-          this.ready = true;
-        } else {
-          // Update top, could have changed in the meantime.
-          this.fixScrollPos(height, document.documentElement.scrollTop);
-        }
-      });
-    },
-    intersectHandler(date, first, last) {
-      let once = true;
-      return (entries) => {
-        const entry = entries[0];
-        if (entry.isIntersecting) {
-          if (entry.boundingClientRect.top <= this.topMargin || first) {
-            console.log("@ ", date.toISODate());
-            this.setDate(date.toISODate());
-          }
-
-          if (once && this.ready && first) {
-            console.log("load up", date.toISODate());
-            this.ready = false;
-            this.fetchMore(
-              date.minus({ days: this.dayIncrement }).toISODate(),
-              date.minus({ days: 1 }).toISODate(),
-              () => {
-                this.fixScrollPos(
-                  document.documentElement.scrollHeight,
-                  document.documentElement.scrollTop,
-                );
-              },
-            );
-            once = false;
-          } else if (once && this.ready && last) {
-            console.log("load down", date.toISODate());
-            this.ready = false;
-            this.fetchMore(
-              date.plus({ days: 1 }).toISODate(),
-              date.plus({ days: this.dayIncrement }).toISODate(),
-              () => {
-                this.ready = true;
-              },
+          if (Object.hasOwn(selectedFilters, "pageType")) {
+            this.absencesExist = true;
+            this.$setToolBarTitle(
+              this.$t(`alsijil.coursebook.title_${selectedFilters.pageType}`),
+              null,
             );
-            once = false;
           }
         }
-      };
+      },
     },
-    // Improve me?
-    // The navigation logic could be a bit simpler if the current days
-    // where known as a sorted array (= result of groupDocsByDay) But
-    // then the list would need its own component and this gets rather
-    // complicated. Then the calendar could also show the present days
-    // / gray out the missing.
-    //
-    // Next two: arg date is ts object
-    findPrev(date) {
-      return this.$refs.days
-        .map((day) => day.date)
-        .sort()
-        .reverse()
-        .find((date2) => date2 < date);
+    itemComponent() {
+      if (this.pageType === "documentations") {
+        return "DocumentationModal";
+      } else {
+        return "DocumentationAbsencesModal";
+      }
     },
-    findNext(date) {
-      return this.$refs.days
-        .map((day) => day.date)
-        .sort()
-        .find((date2) => date2 > date);
+    combinedSelectedParticipations() {
+      return Object.values(this.selectedParticipations).flat();
     },
-    gotoDate(date) {
-      const present = this.$refs.days.find(
-        (day) => day.date.toISODate() === date,
+  },
+  methods: {
+    selectParticipation(id, value) {
+      this.selectedParticipations = Object.assign(
+        {},
+        this.selectedParticipations,
+        { [id]: value },
       );
-
-      if (present) {
-        // React immediatly -> smoother navigation
-        // Also intersect handler does not always react to scrollIntoView
-        this.setDate(date);
-        present.focus("smooth");
-      } else {
-        const prev = this.findPrev(DateTime.fromISO(date));
-        const next = this.findNext(DateTime.fromISO(date));
-        if (prev && next) {
-          // In between two present days -> goto prev
-          this.gotoDate(prev.toISODate());
-        } else {
-          // Outsite present day range
-          this.resetDate(date);
-        }
-      }
     },
-    gotoPrev() {
-      const prev = this.findPrev(DateTime.fromISO(this.currentDate));
-      if (prev) {
-        this.gotoDate(prev.toISODate());
-      }
+    handleMultipleAction(absenceReasonId) {
+      this.loadSelectedParticiptions = true;
+      this.sendToServer(
+        this.combinedSelectedParticipations,
+        "absenceReason",
+        absenceReasonId,
+      );
+      this.$once("save", this.resetMultipleAction);
     },
-    gotoNext() {
-      const next = this.findNext(DateTime.fromISO(this.currentDate));
-      if (next) {
-        this.gotoDate(next.toISODate());
-      }
+    resetMultipleAction() {
+      this.loadSelectedParticiptions = false;
+      this.selectedParticipations = {};
     },
   },
-  created() {
-    this.resetDate();
+  mounted() {
+    this.$setToolBarTitle(
+      this.$t(`alsijil.coursebook.title_${this.pageType}`),
+      null,
+    );
   },
 };
 </script>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookDay.vue b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookDay.vue
deleted file mode 100644
index c4a677c9f5f72227537f7d842baa51902d9859ac..0000000000000000000000000000000000000000
--- a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookDay.vue
+++ /dev/null
@@ -1,72 +0,0 @@
-<template>
-  <v-list-item :style="{ scrollMarginTop: '145px' }" two-line class="px-0">
-    <v-list-item-content>
-      <v-subheader class="text-h6 px-1">{{
-        $d(date, "dateWithWeekday")
-      }}</v-subheader>
-      <v-list max-width="100%" class="pt-0 mt-n1">
-        <v-list-item
-          v-for="doc in docs"
-          :key="'documentation-' + (doc.oldId || doc.id)"
-          class="px-1"
-        >
-          <documentation-modal
-            :documentation="doc"
-            :extra-marks="extraMarks"
-            :affected-query="lastQuery"
-          />
-        </v-list-item>
-      </v-list>
-    </v-list-item-content>
-  </v-list-item>
-</template>
-
-<script>
-import DocumentationModal from "./documentation/DocumentationModal.vue";
-export default {
-  name: "CoursebookDay",
-  components: {
-    DocumentationModal,
-  },
-  props: {
-    date: {
-      type: Object,
-      required: true,
-    },
-    docs: {
-      type: Array,
-      required: true,
-    },
-    lastQuery: {
-      type: Object,
-      required: true,
-    },
-    focusOnMount: {
-      type: Boolean,
-      required: false,
-      default: false,
-    },
-    extraMarks: {
-      type: Array,
-      required: true,
-    },
-  },
-  emits: ["init"],
-  methods: {
-    focus(how) {
-      this.$el.scrollIntoView({
-        behavior: how,
-        block: "start",
-        inline: "nearest",
-      });
-      console.log("focused @", this.date.toISODate());
-    },
-  },
-  mounted() {
-    if (this.focusOnMount) {
-      this.$nextTick(this.focus("instant"));
-      this.$emit("init");
-    }
-  },
-};
-</script>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookEmptyMessage.vue b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookEmptyMessage.vue
deleted file mode 100644
index 346ecc63c272ab5c57a1da9f5e5f78b825739a79..0000000000000000000000000000000000000000
--- a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookEmptyMessage.vue
+++ /dev/null
@@ -1,25 +0,0 @@
-<template>
-  <v-list-item>
-    <v-list-item-content
-      class="d-flex justify-center align-center flex-column full-width"
-    >
-      <div class="mb-4">
-        <v-icon large color="primary">{{ icon }}</v-icon>
-      </div>
-      <v-list-item-title>
-        <slot></slot>
-      </v-list-item-title>
-    </v-list-item-content>
-  </v-list-item>
-</template>
-<script>
-export default {
-  name: "CoursebookEmptyMessage",
-  props: {
-    icon: {
-      type: String,
-      default: "mdi-book-alert-outline",
-    },
-  },
-};
-</script>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookFilters.vue b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookFilters.vue
index 00ff6b02fe2420d21396c500d20eca9a0f4678b1..6b9dcc92fb433ff320f0c76790656c7b76c0472f 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookFilters.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookFilters.vue
@@ -1,5 +1,7 @@
 <template>
-  <div class="d-flex flex-grow-1 justify-end">
+  <div
+    class="d-flex flex-column flex-sm-row flex-nowrap flex-grow-1 justify-end gap align-stretch"
+  >
     <v-autocomplete
       :items="selectable"
       item-text="name"
@@ -16,7 +18,7 @@
       @click:clear="selectObject"
       class="max-width"
     />
-    <div class="ml-6">
+    <div class="mx-6">
       <v-switch
         :loading="selectLoading"
         :label="$t('alsijil.coursebook.filter.own')"
@@ -39,7 +41,29 @@
         inset
         hide-details
       />
+      <v-switch
+        v-if="pageType === 'absences'"
+        :loading="selectLoading"
+        :label="$t('alsijil.coursebook.filter.absences_exist')"
+        :input-value="value.absencesExist"
+        @change="
+          $emit('input', {
+            absencesExist: $event,
+          })
+        "
+        dense
+        inset
+        hide-details
+      />
     </div>
+    <v-btn
+      outlined
+      color="primary"
+      :loading="selectLoading"
+      @click="togglePageType()"
+    >
+      {{ pageTypeButtonText }}
+    </v-btn>
   </div>
 </template>
 
@@ -64,6 +88,11 @@ export default {
       type: Object,
       required: true,
     },
+    pageType: {
+      type: String,
+      required: false,
+      default: "documentations",
+    },
   },
   emits: ["input"],
   apollo: {
@@ -96,6 +125,13 @@ export default {
           o.id === this.value.objId,
       );
     },
+    pageTypeButtonText() {
+      if (this.value.pageType === "documentations") {
+        return this.$t("alsijil.coursebook.filter.page_type.absences");
+      } else {
+        return this.$t("alsijil.coursebook.filter.page_type.documentations");
+      }
+    },
   },
   methods: {
     selectObject(selection) {
@@ -111,6 +147,16 @@ export default {
         objId: this.value.objId,
       });
     },
+    togglePageType() {
+      this.$emit("input", {
+        pageType:
+          this.value.pageType === "documentations"
+            ? "absences"
+            : "documentations",
+        objType: this.value.objType,
+        objId: this.value.objId,
+      });
+    },
   },
 };
 </script>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/DocumentationAbsences.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/DocumentationAbsences.vue
new file mode 100644
index 0000000000000000000000000000000000000000..ef97f2ddb73cd0eb5714e1a8818e97fb54fde67a
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/DocumentationAbsences.vue
@@ -0,0 +1,110 @@
+<template>
+  <v-card :class="{ 'my-1 full-width': true, 'd-flex flex-column': !compact }">
+    <v-card-title v-if="!compact">
+      <lesson-information v-bind="documentationPartProps" />
+    </v-card-title>
+
+    <v-card-text
+      class="full-width main-body"
+      :class="{
+        vertical: !compact || $vuetify.breakpoint.mobile,
+        'pa-2': compact,
+      }"
+    >
+      <lesson-information v-if="compact" v-bind="documentationPartProps" />
+
+      <lesson-notes class="span-2" v-bind="documentationPartProps" />
+      <participation-list
+        :include-present="false"
+        class="participation-list"
+        v-bind="documentationPartProps"
+        :value="value"
+        @input="$emit('input', $event)"
+      />
+    </v-card-text>
+    <v-spacer />
+    <v-divider />
+    <v-card-actions v-if="!compact">
+      <v-spacer />
+      <cancel-button
+        v-if="documentation.canEdit"
+        @click="$emit('close')"
+        :disabled="loading"
+      />
+      <save-button
+        v-if="documentation.canEdit"
+        @click="save"
+        :loading="loading"
+      />
+      <cancel-button
+        v-if="!documentation.canEdit"
+        i18n-key="actions.close"
+        @click="$emit('close')"
+      />
+    </v-card-actions>
+  </v-card>
+</template>
+
+<script>
+import ParticipationList from "./ParticipationList.vue";
+import LessonInformation from "../documentation/LessonInformation.vue";
+import LessonNotes from "../documentation/LessonNotes.vue";
+
+import SaveButton from "aleksis.core/components/generic/buttons/SaveButton.vue";
+import CancelButton from "aleksis.core/components/generic/buttons/CancelButton.vue";
+
+import { createOrUpdateDocumentations } from "../coursebook.graphql";
+
+import documentationPartMixin from "../documentation/documentationPartMixin";
+
+export default {
+  name: "DocumentationAbsences",
+  components: {
+    ParticipationList,
+    LessonInformation,
+    LessonNotes,
+    SaveButton,
+    CancelButton,
+  },
+  emits: ["open", "close"],
+  mixins: [documentationPartMixin],
+  data() {
+    return {
+      loading: false,
+      documentationsMutation: createOrUpdateDocumentations,
+      selectedParticipations: [],
+    };
+  },
+  props: {
+    value: {
+      type: Array,
+      required: true,
+    },
+  },
+  methods: {
+    save() {
+      this.$refs.summary.save();
+      this.$emit("close");
+    },
+  },
+};
+</script>
+
+<style scoped>
+.main-body {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  grid-template-rows: min-content min-content;
+  column-gap: 1em;
+}
+.participation-list {
+  grid-column-start: 1;
+  grid-column-end: span 3;
+}
+.span-2 {
+  grid-column-end: span 2;
+}
+.vertical > * {
+  grid-column-end: span 3;
+}
+</style>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/DocumentationAbsencesModal.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/DocumentationAbsencesModal.vue
new file mode 100644
index 0000000000000000000000000000000000000000..1e092b4ee14c410f5f5a0f04cc8bec9e57c2a6c3
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/DocumentationAbsencesModal.vue
@@ -0,0 +1,45 @@
+<!-- Wrapper around DocumentationAbsences.vue -->
+<!-- That uses it either as list item or as editable modal dialog. -->
+<template>
+  <mobile-fullscreen-dialog v-model="popup" max-width="500px">
+    <template #activator="activator">
+      <!-- list view -> activate dialog -->
+      <documentation-absences
+        compact
+        v-bind="$attrs"
+        :dialog-activator="activator"
+        :value="value"
+        @input="$emit('input', $event)"
+      />
+    </template>
+    <!-- dialog view -> deactivate dialog -->
+    <!-- cancel | save (through lesson-summary) -->
+    <documentation v-bind="$attrs" @close="popup = false" />
+  </mobile-fullscreen-dialog>
+</template>
+
+<script>
+import MobileFullscreenDialog from "aleksis.core/components/generic/dialogs/MobileFullscreenDialog.vue";
+import DocumentationAbsences from "./DocumentationAbsences.vue";
+import Documentation from "../documentation/Documentation.vue";
+
+export default {
+  name: "DocumentationAbsencesModal",
+  components: {
+    MobileFullscreenDialog,
+    Documentation,
+    DocumentationAbsences,
+  },
+  data() {
+    return {
+      popup: false,
+    };
+  },
+  props: {
+    value: {
+      type: Array,
+      required: true,
+    },
+  },
+};
+</script>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue
index 3e7820f39eb4608ad9c3f4512c5934e491d48335..aa9176a8bbf4e3bb61c6641580e1eb5c5b96f980 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue
@@ -4,11 +4,9 @@ import AbsenceReasonChip from "aleksis.apps.kolego/components/AbsenceReasonChip.
 import AbsenceReasonGroupSelect from "aleksis.apps.kolego/components/AbsenceReasonGroupSelect.vue";
 import DialogCloseButton from "aleksis.core/components/generic/buttons/DialogCloseButton.vue";
 import MobileFullscreenDialog from "aleksis.core/components/generic/dialogs/MobileFullscreenDialog.vue";
-import mutateMixin from "aleksis.core/mixins/mutateMixin.js";
+import updateParticipationMixin from "./updateParticipationMixin.js";
 import deepSearchMixin from "aleksis.core/mixins/deepSearchMixin.js";
-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";
@@ -31,7 +29,7 @@ export default {
     TardinessField,
     DialogCloseButton,
   },
-  mixins: [documentationPartMixin, mutateMixin, deepSearchMixin],
+  mixins: [updateParticipationMixin, deepSearchMixin],
   data() {
     return {
       dialog: false,
@@ -59,48 +57,6 @@ export default {
     },
   },
   methods: {
-    sendToServer(participations, field, value) {
-      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,
-            ...fieldValue,
-          })),
-        },
-        (storedDocumentations, incomingStatuses) => {
-          const documentation = storedDocumentations.find(
-            (doc) => doc.id === this.documentation.id,
-          );
-
-          incomingStatuses.forEach((newStatus) => {
-            const participationStatus = documentation.participations.find(
-              (part) => part.id === newStatus.id,
-            );
-            participationStatus.absenceReason = newStatus.absenceReason;
-            participationStatus.tardiness = newStatus.tardiness;
-            participationStatus.isOptimistic = newStatus.isOptimistic;
-          });
-
-          return storedDocumentations;
-        },
-      );
-    },
     handleMultipleAction(absenceReasonId) {
       this.loadSelected = true;
       this.sendToServer(this.selected, "absenceReason", absenceReasonId);
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/ParticipationList.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/ParticipationList.vue
new file mode 100644
index 0000000000000000000000000000000000000000..0b3f650d805699c2bf18c217a35d85f736f0aa4f
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/ParticipationList.vue
@@ -0,0 +1,96 @@
+<script setup>
+import AbsenceReasonGroupSelect from "aleksis.apps.kolego/components/AbsenceReasonGroupSelect.vue";
+</script>
+
+<template>
+  <v-list v-if="filteredParticipations.length">
+    <v-divider />
+
+    <v-list-item-group :value="value" multiple @change="changeSelect">
+      <template v-for="(participation, index) in filteredParticipations">
+        <v-list-item
+          :key="`documentation-${documentation.id}-participation-${participation.id}`"
+          :value="participation.id"
+          v-bind="$attrs"
+          two-line
+        >
+          <template #default="{ active }">
+            <v-list-item-action>
+              <v-checkbox :input-value="active" />
+            </v-list-item-action>
+            <v-list-item-content>
+              <v-list-item-title>
+                {{ participation.person.fullName }}
+              </v-list-item-title>
+              <absence-reason-group-select
+                v-if="participation.absenceReason && !compact"
+                class="full-width"
+                allow-empty
+                empty-value="present"
+                :loadSelectedChip="loading"
+                :value="participation.absenceReason?.id || 'present'"
+                @input="sendToServer([participation], 'absenceReason', $event)"
+              />
+            </v-list-item-content>
+            <v-list-item-action v-if="participation.absenceReason && compact">
+              <absence-reason-group-select
+                allow-empty
+                empty-value="present"
+                :loadSelectedChip="loading"
+                :value="participation.absenceReason?.id || 'present'"
+                @input="sendToServer([participation], 'absenceReason', $event)"
+              />
+            </v-list-item-action>
+          </template>
+        </v-list-item>
+        <v-divider
+          v-if="index < filteredParticipations.length - 1"
+          :key="index"
+        ></v-divider>
+      </template>
+    </v-list-item-group>
+  </v-list>
+</template>
+
+<script>
+import updateParticipationMixin from "./updateParticipationMixin";
+
+export default {
+  name: "ParticipationList",
+  mixins: [updateParticipationMixin],
+  data() {
+    return {
+      loading: false,
+      participationDialogs: false,
+      isExpanded: false,
+    };
+  },
+  props: {
+    includePresent: {
+      type: Boolean,
+      required: false,
+      default: true,
+    },
+    value: {
+      type: Array,
+      required: true,
+    },
+  },
+  computed: {
+    filteredParticipations() {
+      if (!this.includePresent) {
+        return this.documentation.participations.filter(
+          (p) => !!p.absenceReason,
+        );
+      } else {
+        return this.documentation.participations;
+      }
+    },
+  },
+  methods: {
+    changeSelect(value) {
+      this.$emit("input", value);
+    },
+  },
+};
+</script>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql
index dd50495972c020550dec310081f0c8c149473d9e..ce296d2b684c1f45928e1d94e8e584f7b845bb69 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql
@@ -4,7 +4,6 @@ mutation updateParticipationStatuses(
   updateParticipationStatuses(input: $input) {
     items: participationStatuses {
       id
-      isOptimistic
       relatedDocumentation {
         id
       }
@@ -14,7 +13,19 @@ mutation updateParticipationStatuses(
         shortName
         colour
       }
+      notesWithExtraMark {
+        id
+        extraMark {
+          id
+          showInCoursebook
+        }
+      }
+      notesWithNote {
+        id
+        note
+      }
       tardiness
+      isOptimistic
     }
   }
 }
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/selectParticipationMixin.js b/aleksis/apps/alsijil/frontend/components/coursebook/absences/selectParticipationMixin.js
new file mode 100644
index 0000000000000000000000000000000000000000..2d6d522692690c6def6aaa53e2dc0e7835019681
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/selectParticipationMixin.js
@@ -0,0 +1,27 @@
+/**
+ * Mixin to provide passing through functionality for the events emitted when (de)selecting participations on the absence overview page
+ */
+export default {
+  emits: ["select", "deselect"],
+  methods: {
+    handleSelect(participation) {
+      this.$emit("select", participation);
+    },
+    handleDeselect(participation) {
+      this.$emit("deselect", participation);
+    },
+  },
+
+  computed: {
+    /**
+     * All necessary listeners bundled together to easily pass to child components
+     * @returns {{select: Function, deselect: Function}}
+     */
+    selectListeners() {
+      return {
+        select: this.handleSelect,
+        deselect: this.handleDeselect,
+      };
+    },
+  },
+};
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/sendToServerMixin.js b/aleksis/apps/alsijil/frontend/components/coursebook/absences/sendToServerMixin.js
new file mode 100644
index 0000000000000000000000000000000000000000..d75f72173b80315705e6cf3f86fe614caeafe051
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/sendToServerMixin.js
@@ -0,0 +1,54 @@
+/**
+ * Mixin to provide shared functionality needed to send updated participation data to the server
+ */
+import { updateParticipationStatuses } from "./participationStatus.graphql";
+import mutateMixin from "aleksis.core/mixins/mutateMixin.js";
+
+export default {
+  mixins: [mutateMixin],
+  methods: {
+    sendToServer(participations, field, value) {
+      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 || participation,
+            ...fieldValue,
+          })),
+        },
+        (storedDocumentations, incomingStatuses) => {
+          // TODO: what should happen here in places where there is more than one documentation?
+          const documentation = storedDocumentations.find(
+            (doc) => doc.id === this.documentation.id,
+          );
+
+          incomingStatuses.forEach((newStatus) => {
+            const participationStatus = documentation.participations.find(
+              (part) => part.id === newStatus.id,
+            );
+            participationStatus.absenceReason = newStatus.absenceReason;
+            participationStatus.tardiness = newStatus.tardiness;
+            participationStatus.isOptimistic = newStatus.isOptimistic;
+          });
+
+          return storedDocumentations;
+        },
+      );
+    },
+  },
+};
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/updateParticipationMixin.js b/aleksis/apps/alsijil/frontend/components/coursebook/absences/updateParticipationMixin.js
new file mode 100644
index 0000000000000000000000000000000000000000..2ed2c537d8bda9d11b2757468f72ad19cb89b7c6
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/updateParticipationMixin.js
@@ -0,0 +1,9 @@
+/**
+ * Mixin to provide shared functionality needed to update participations
+ */
+import documentationPartMixin from "../documentation/documentationPartMixin";
+import sendToServerMixin from "./sendToServerMixin";
+
+export default {
+  mixins: [documentationPartMixin, sendToServerMixin],
+};
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/coursebook.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/coursebook.graphql
index 73f0dc3c281a5e03cfbcd7d29dff9f8dc2e61d2b..f78545d8db703b193e313cb36f7293d419bbcd92 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/coursebook.graphql
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/coursebook.graphql
@@ -19,6 +19,7 @@ query documentationsForCoursebook(
   $dateStart: Date!
   $dateEnd: Date!
   $incomplete: Boolean
+  $absencesExist: Boolean
 ) {
   items: documentationsForCoursebook(
     own: $own
@@ -27,6 +28,7 @@ query documentationsForCoursebook(
     dateStart: $dateStart
     dateEnd: $dateEnd
     incomplete: $incomplete
+    absencesExist: $absencesExist
   ) {
     id
     course {
@@ -130,6 +132,17 @@ mutation createOrUpdateDocumentations($input: [DocumentationInputType]!) {
           shortName
           colour
         }
+        notesWithExtraMark {
+          id
+          extraMark {
+            id
+            showInCoursebook
+          }
+        }
+        notesWithNote {
+          id
+          note
+        }
         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 630085bd593f145e5a8a15c50d686a2e48cf6a20..a679830c89c0b3fd77a5d712efaea5931b24332c 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/documentation/DocumentationModal.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/documentation/DocumentationModal.vue
@@ -13,7 +13,11 @@
     </template>
     <!-- dialog view -> deactivate dialog -->
     <!-- cancel | save (through lesson-summary) -->
-    <documentation v-bind="$attrs" @close="popup = false" />
+    <documentation
+      v-bind="$attrs"
+      :extra-marks="extraMarks"
+      @close="popup = false"
+    />
   </mobile-fullscreen-dialog>
 </template>
 
diff --git a/aleksis/apps/alsijil/frontend/index.js b/aleksis/apps/alsijil/frontend/index.js
index e73b19d83631d0953a269f037fc064257bb187d4..fb69e3eacf436cc238afef95e3777f9c218f9480 100644
--- a/aleksis/apps/alsijil/frontend/index.js
+++ b/aleksis/apps/alsijil/frontend/index.js
@@ -24,6 +24,7 @@ export default {
           name: "alsijil.coursebook",
           params: {
             filterType: "my",
+            pageType: "documentations",
           },
           hash: "#" + DateTime.now().toISODate(),
         };
@@ -40,7 +41,7 @@ export default {
       },
       children: [
         {
-          path: ":filterType(my|all)/:objType(group|course|teacher)?/:objId(\\d+)?/",
+          path: ":pageType(documentations|absences)/:filterType(my|all)/:objType(group|course|teacher)?/:objId(\\d+)?/",
           component: () => import("./components/coursebook/Coursebook.vue"),
           name: "alsijil.coursebook",
           meta: {
diff --git a/aleksis/apps/alsijil/frontend/messages/de.json b/aleksis/apps/alsijil/frontend/messages/de.json
index c2b56f0159cb184f5b9fd49d8505fc4771bc2d92..8f70c6b9cd87fe46727c705bd85d9a041e2c3ea4 100644
--- a/aleksis/apps/alsijil/frontend/messages/de.json
+++ b/aleksis/apps/alsijil/frontend/messages/de.json
@@ -61,6 +61,8 @@
           }
         }
       },
+      "title_absences": "Kursbuch · Abwesenheiten",
+      "title_documentations": "Kursbuch",
       "title_plural": "Kursbuch"
     },
     "excuse_types": {
diff --git a/aleksis/apps/alsijil/frontend/messages/en.json b/aleksis/apps/alsijil/frontend/messages/en.json
index 3ad99b40cadee92070cf5a2b1ab287343b2a8e6f..17769c47dbb20ca106eab4f05b046c51c158498a 100644
--- a/aleksis/apps/alsijil/frontend/messages/en.json
+++ b/aleksis/apps/alsijil/frontend/messages/en.json
@@ -45,6 +45,8 @@
       "menu_title": "Coursebook",
       "page_title": "Coursebook for {name}",
       "title_plural": "Coursebook",
+      "title_documentations": "Coursebook",
+      "title_absences": "Coursebook · Absences",
       "status": {
         "available": "Documentation available",
         "missing": "Documentation missing",
@@ -81,12 +83,18 @@
         "missing": "Only show incomplete lessons",
         "groups": "Groups",
         "courses": "Courses",
-        "filter_for_obj": "Filter for group and course"
+        "filter_for_obj": "Filter for group and course",
+        "page_type": {
+          "documentations": "Show documentations",
+          "absences": "Show absences"
+        },
+        "absences_exist": "Only show lessons with absent participants"
       },
       "present_number": "{present}/{total} present",
       "no_data": "No lessons for the selected groups and courses in this period",
       "no_results": "No search results for {search}",
       "absences": {
+        "action_for_selected": "Mark selected participant as: | Mark {count} selected participants as",
         "title": "Register absences",
         "button": "Register absences",
         "summary": "Summary",
diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py
index 56406ce4e9ff8e8dc0a3e2995d15eef45d8044a7..63b4805f7009ba72fcfe42ab6bb6b17635aa2940 100644
--- a/aleksis/apps/alsijil/models.py
+++ b/aleksis/apps/alsijil/models.py
@@ -534,6 +534,7 @@ class Documentation(CalendarEvent):
         datetime_end: datetime,
         events: list,
         incomplete: Optional[bool] = False,
+        absences_exist: Optional[bool] = False,
     ) -> tuple:
         """Get all the documentations for the events.
         Create dummy documentations if none exist.
@@ -563,10 +564,16 @@ class Documentation(CalendarEvent):
 
             doc = next(existing_documentations_event, None)
             if doc:
-                if incomplete and doc.topic:
+                if (incomplete and doc.topic) or (
+                    absences_exist
+                    and (
+                        not doc.participations.all()
+                        or not [d for d in doc.participations.all() if d.absence_reason]
+                    )
+                ):
                     continue
                 docs.append(doc)
-            else:
+            elif not absences_exist:
                 if event_reference_obj.amends:
                     if event_reference_obj.course:
                         course = event_reference_obj.course
diff --git a/aleksis/apps/alsijil/schema/__init__.py b/aleksis/apps/alsijil/schema/__init__.py
index 681b0cd3796d2ee86c219a6e9bc4ec562e484752..49bfbe7abb2b04515c30ccd931c4d9d847578caf 100644
--- a/aleksis/apps/alsijil/schema/__init__.py
+++ b/aleksis/apps/alsijil/schema/__init__.py
@@ -51,6 +51,7 @@ class Query(graphene.ObjectType):
         date_start=graphene.Date(required=True),
         date_end=graphene.Date(required=True),
         incomplete=graphene.Boolean(required=False),
+        absences_exist=graphene.Boolean(required=False),
     )
 
     groups_by_person = FilterOrderList(GroupType, person=graphene.ID())
@@ -81,6 +82,7 @@ class Query(graphene.ObjectType):
         obj_type=None,
         obj_id=None,
         incomplete=False,
+        absences_exist=False,
         **kwargs,
     ):
         if (
@@ -131,6 +133,7 @@ class Query(graphene.ObjectType):
             datetime.combine(date_end, datetime.max.time()),
             events,
             incomplete,
+            absences_exist,
         )
         return docs + dummies