diff --git a/aleksis/apps/alsijil/actions.py b/aleksis/apps/alsijil/actions.py
index ec80a8cf510c579ab09171e29f0d3163618f9522..5e7da3a3207965fc8ddb176fef5581784674885e 100644
--- a/aleksis/apps/alsijil/actions.py
+++ b/aleksis/apps/alsijil/actions.py
@@ -1,4 +1,4 @@
-from typing import Sequence
+from typing import Callable, Sequence
 
 from django.contrib import messages
 from django.contrib.humanize.templatetags.humanize import apnumber
@@ -10,6 +10,37 @@ from django.utils.translation import gettext_lazy as _
 from aleksis.core.models import Notification
 
 
+def mark_as_excused(modeladmin, request, queryset):
+    queryset.update(excused=True, excuse_type=None)
+
+
+mark_as_excused.short_description = _("Mark as excused")
+
+
+def mark_as_unexcused(modeladmin, request, queryset):
+    queryset.update(excused=False, excuse_type=None)
+
+
+mark_as_unexcused.short_description = _("Mark as unexcused")
+
+
+def mark_as_excuse_type_generator(excuse_type) -> Callable:
+    def mark_as_excuse_type(modeladmin, request, queryset):
+        queryset.update(excused=True, excuse_type=excuse_type)
+
+    mark_as_excuse_type.short_description = _(f"Mark as {excuse_type.name}")
+    mark_as_excuse_type.__name__ = f"mark_as_excuse_type_{excuse_type.short_name}"
+
+    return mark_as_excuse_type
+
+
+def delete_personal_note(modeladmin, request, queryset):
+    queryset.delete()
+
+
+delete_personal_note.short_description = _("Delete")
+
+
 def send_request_to_check_entry(modeladmin, request: HttpRequest, selected_items: Sequence[dict]):
     """Send notifications to the teachers of the selected register objects.
 
diff --git a/aleksis/apps/alsijil/filters.py b/aleksis/apps/alsijil/filters.py
new file mode 100644
index 0000000000000000000000000000000000000000..4033e36e5500e3c440b73950fb96741761c46acf
--- /dev/null
+++ b/aleksis/apps/alsijil/filters.py
@@ -0,0 +1,33 @@
+from django.utils.translation import gettext as _
+
+from django_filters import CharFilter, DateFilter, FilterSet
+from material import Layout, Row
+
+from .models import PersonalNote
+
+
+class PersonalNoteFilter(FilterSet):
+    day_start = DateFilter(lookup_expr="gte", label=_("After"))
+    day_end = DateFilter(lookup_expr="lte", label=_("Before"))
+    subject = CharFilter(lookup_expr="icontains", label=_("Subject"))
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.form.fields["late__lt"].label = _("Tardiness is lower than")
+        self.form.fields["late__gt"].label = _("Tardiness is bigger than")
+        self.form.layout = Layout(
+            Row("subject"),
+            Row("day_start", "day_end"),
+            Row("absent", "excused", "excuse_type"),
+            Row("late__gt", "late__lt", "extra_marks"),
+        )
+
+    class Meta:
+        model = PersonalNote
+        fields = {
+            "excused": ["exact"],
+            "late": ["lt", "gt"],
+            "absent": ["exact"],
+            "excuse_type": ["exact"],
+            "extra_marks": ["exact"],
+        }
diff --git a/aleksis/apps/alsijil/forms.py b/aleksis/apps/alsijil/forms.py
index 322de9d2fa763a3be9cec5fab8fae30c8550a1ec..53b5ffee1c68c2b0645d86753e0eb8eb934d2c0c 100644
--- a/aleksis/apps/alsijil/forms.py
+++ b/aleksis/apps/alsijil/forms.py
@@ -14,12 +14,18 @@ from material import Fieldset, Layout, Row
 
 from aleksis.apps.chronos.managers import TimetableType
 from aleksis.apps.chronos.models import Subject, TimePeriod
-from aleksis.core.forms import ListActionForm
+from aleksis.core.forms import ActionForm, ListActionForm
 from aleksis.core.models import Group, Person, SchoolTerm
 from aleksis.core.util.core_helpers import get_site_preferences
 from aleksis.core.util.predicates import check_global_permission
 
-from .actions import send_request_to_check_entry
+from .actions import (
+    delete_personal_note,
+    mark_as_excuse_type_generator,
+    mark_as_excused,
+    mark_as_unexcused,
+    send_request_to_check_entry,
+)
 from .models import (
     ExcuseType,
     ExtraMark,
@@ -175,6 +181,18 @@ class ExcuseTypeForm(forms.ModelForm):
         fields = ["short_name", "name"]
 
 
+class PersonOverviewForm(ActionForm):
+    def get_actions(self):
+        return (
+            [mark_as_excused, mark_as_unexcused]
+            + [
+                mark_as_excuse_type_generator(excuse_type)
+                for excuse_type in ExcuseType.objects.all()
+            ]
+            + [delete_personal_note]
+        )
+
+
 class GroupRoleForm(forms.ModelForm):
     layout = Layout("name", "icon", "colour")
 
diff --git a/aleksis/apps/alsijil/managers.py b/aleksis/apps/alsijil/managers.py
index ab9daaa7ebb171b9979fa8893fc7eaa0c598a31d..69cc3fd7d62e3669e770ea12468490bf08ea1009 100644
--- a/aleksis/apps/alsijil/managers.py
+++ b/aleksis/apps/alsijil/managers.py
@@ -6,6 +6,7 @@ from django.db.models.fields import DateField
 from django.db.models.functions import Concat
 from django.db.models.query import Prefetch
 from django.db.models.query_utils import Q
+from django.utils.translation import gettext as _
 
 from calendarweek import CalendarWeek
 
@@ -71,6 +72,16 @@ class RegisterObjectRelatedQuerySet(QuerySet):
             ),
         )
 
+    def annotate_subject(self) -> QuerySet:
+        """Annotate lesson documentations with the subjects."""
+        return self.annotate(
+            subject=Case(
+                When(lesson_period__isnull=False, then="lesson_period__lesson__subject__name",),
+                When(extra_lesson__isnull=False, then="extra_lesson__subject__name",),
+                default=Value(_("Event")),
+            )
+        )
+
 
 class PersonalNoteManager(CurrentSiteManagerWithoutMigrations):
     """Manager adding specific methods to personal notes."""
diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py
index 4eecb39f4ca4290c3d650d333a78f22d2dc76279..94044b74a3f3c859cd95ae19290e375baf9ee595 100644
--- a/aleksis/apps/alsijil/models.py
+++ b/aleksis/apps/alsijil/models.py
@@ -30,7 +30,7 @@ from aleksis.apps.alsijil.managers import (
 )
 from aleksis.apps.chronos.managers import GroupPropertiesMixin
 from aleksis.apps.chronos.mixins import WeekRelatedMixin
-from aleksis.apps.chronos.models import Event, ExtraLesson, LessonPeriod
+from aleksis.apps.chronos.models import Event, ExtraLesson, LessonPeriod, TimePeriod
 from aleksis.core.mixins import ExtensibleModel, GlobalPermissionModel
 from aleksis.core.models import SchoolTerm
 from aleksis.core.util.core_helpers import get_site_preferences
@@ -155,6 +155,33 @@ class RegisterObjectRelatedMixin(WeekRelatedMixin):
             else f"{date_format(self.event.date_start)}–{date_format(self.event.date_end)}"
         )
 
+    @property
+    def period(self: Union["LessonDocumentation", "PersonalNote"]) -> TimePeriod:
+        """Get the date of this lesson documentation or personal note.
+
+        :: warning::
+
+            As events can be longer than one day,
+            this will return `None` for events.
+        """
+        if self.event:
+            return self.event.period_from
+        else:
+            return self.register_object.period
+
+    @property
+    def period_formatted(self: Union["LessonDocumentation", "PersonalNote"]) -> str:
+        """Get a formatted version of the period of this object.
+
+        Lesson periods, extra lessons: formatted period
+        Events: formatted period range
+        """
+        return (
+            f"{self.period.period}."
+            if not self.event
+            else f"{self.event.period_from.period}.–{self.event.period_to.period}."
+        )
+
     def get_absolute_url(self: Union["LessonDocumentation", "PersonalNote"]) -> str:
         """Get the absolute url of the detail view for the related register object."""
         return self.register_object.get_alsijil_url(self.calendar_week)
diff --git a/aleksis/apps/alsijil/static/css/alsijil/person.css b/aleksis/apps/alsijil/static/css/alsijil/person.css
new file mode 100644
index 0000000000000000000000000000000000000000..fdba89c2808ccb46cc1535b3f27545c366af6c68
--- /dev/null
+++ b/aleksis/apps/alsijil/static/css/alsijil/person.css
@@ -0,0 +1,95 @@
+span.input-field.inline > .select-wrapper > input {
+    color: red;
+    padding: 14px 0 0 0;
+    line-height: 2px;
+    height: 36px;
+    vertical-align: middle;
+}
+
+span.input-field.inline > .select-wrapper .caret {
+    top: 12px !important;
+}
+
+@media screen and (min-width: 1400px) {
+    li.collection-item form {
+        margin: -30px 0 -30px 0;
+    }
+
+    li.collection-item#title #select_all_span {
+        margin-top: 5px;
+    }
+}
+
+.collection {
+    overflow: visible;
+    overflow-x: hidden;
+}
+
+#select_all_container {
+    display: none;
+}
+
+#select_all_box:indeterminate + span:not(.lever):before {
+    top: -4px;
+    left: -6px;
+    width: 10px;
+    height: 12px;
+    border-top: none;
+    border-left: none;
+    border-right: white 2px solid;
+    border-bottom: none;
+    transform: rotate(90deg);
+    backface-visibility: hidden;
+    transform-origin: 100% 100%;
+
+}
+
+#select_all_box:indeterminate + span:not(.lever):after {
+    top: 0;
+    width: 20px;
+    height: 20px;
+    border: 2px solid currentColor;
+    background-color: currentColor;
+    z-index: 0;
+}
+
+#select_all_box_text {
+    color: #9e9e9e !important;
+}
+
+td.material-icons {
+    display: table-cell;
+}
+
+.medium-high {
+    position: relative;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, 50%);
+}
+
+@media screen and (min-width: 600px) {
+    /* On medium and up devices */
+    .medium-high-right {
+        float: right;
+        transform: translate(0%, 50%);
+    }
+}
+
+@media screen and (max-width: 600px) {
+    /* Only on small devices */
+    .full-width-s {
+        width: 100%;
+    }
+
+    #heading {
+        display: block;
+    }
+    #heading + a {
+        float: none!important;
+    }
+}
+
+.overflow-x-scroll {
+    overflow-x: scroll;
+}
diff --git a/aleksis/apps/alsijil/tables.py b/aleksis/apps/alsijil/tables.py
index 336ca5827932b81fab534ab277ee5e2dab0aad6d..b0337d749297affafcf487b3936aec0efa4c0789 100644
--- a/aleksis/apps/alsijil/tables.py
+++ b/aleksis/apps/alsijil/tables.py
@@ -1,11 +1,15 @@
 from django.template.loader import render_to_string
+from django.utils.safestring import mark_safe
 from django.utils.translation import gettext_lazy as _
 
 import django_tables2 as tables
 from django_tables2.utils import A
 
+from aleksis.apps.chronos.models import Event, LessonPeriod
 from aleksis.core.util.tables import SelectColumn
 
+from .models import PersonalNote
+
 
 class ExtraMarkTable(tables.Table):
     class Meta:
@@ -82,6 +86,87 @@ class GroupRoleTable(tables.Table):
             self.columns.hide("delete")
 
 
+class PersonalNoteTable(tables.Table):
+    selected = SelectColumn(attrs={"input": {"name": "selected_objects"}}, accessor=A("pk"))
+    date = tables.Column(
+        verbose_name=_("Date"), accessor=A("date_formatted"), order_by=A("day_start")
+    )
+    period = tables.Column(
+        verbose_name=_("Period"), accessor=A("period_formatted"), order_by=A("order_period")
+    )
+    groups = tables.Column(
+        verbose_name=_("Groups"),
+        accessor=A("register_object__group_names"),
+        order_by=A("order_groups"),
+    )
+    teachers = tables.Column(
+        verbose_name=_("Teachers"),
+        accessor=A("register_object__teacher_names"),
+        order_by=A("order_teachers"),
+    )
+    subject = tables.Column(verbose_name=_("Subject"), accessor=A("subject"))
+    absent = tables.Column()
+    late = tables.Column()
+    excused = tables.Column(verbose_name=_("Excuse"))
+    extra_marks = tables.Column(verbose_name="Extra marks", accessor=A("extra_marks__all"))
+
+    def render_groups(self, value, record):
+        if isinstance(record.register_object, LessonPeriod):
+            return record.register_object.lesson.group_names
+        else:
+            return value
+
+    def render_subject(self, value, record):
+        if isinstance(record.register_object, Event):
+            return _("Event")
+        else:
+            return value
+
+    def render_absent(self, value):
+        return (
+            render_to_string(
+                "components/materialize-chips.html",
+                dict(content="Absent", classes="red white-text"),
+            )
+            if value
+            else "–"
+        )
+
+    def render_excused(self, value, record):
+        if record.absent and value:
+            context = dict(content=_("Excused"), classes="green white-text")
+            badge = render_to_string("components/materialize-chips.html", context)
+            if record.excuse_type:
+                context = dict(content=record.excuse_type.name, classes="green white-text")
+                badge = render_to_string("components/materialize-chips.html", context)
+            return badge
+        return "–"
+
+    def render_late(self, value):
+        if value:
+            content = _(f"{value}' late")
+            context = dict(content=content, classes="orange white-text")
+            return render_to_string("components/materialize-chips.html", context)
+        else:
+            return "–"
+
+    def render_extra_marks(self, value):
+        if value:
+            badges = ""
+            for extra_mark in value:
+                content = extra_mark.name
+                badges += render_to_string(
+                    "components/materialize-chips.html", context=dict(content=content)
+                )
+            return mark_safe(badges)  # noqa
+        else:
+            return "–"
+
+    class Meta:
+        model = PersonalNote
+        fields = ()
+
+
 def _get_link(value, record):
     return record["register_object"].get_alsijil_url(record.get("week"))
 
diff --git a/aleksis/apps/alsijil/templates/alsijil/class_register/person.html b/aleksis/apps/alsijil/templates/alsijil/class_register/person.html
index 65c38961a07a425c5b611e0b94683bf73b608cd6..4d063d3ddaf1dc67da0ae29a5d0ac2e0b4c67911 100644
--- a/aleksis/apps/alsijil/templates/alsijil/class_register/person.html
+++ b/aleksis/apps/alsijil/templates/alsijil/class_register/person.html
@@ -1,6 +1,11 @@
 {# -*- engine:django -*- #}
 {% extends "core/base.html" %}
-{% load rules data_helpers week_helpers i18n material_form django_tables2 %}
+{% load rules data_helpers week_helpers i18n material_form static django_tables2 %}
+
+{% block extra_head %}
+  <link rel="stylesheet" href="{% static "css/alsijil/person.css" %}">
+  <script src="{% static "js/multi_select.js" %}" type="text/javascript"></script>
+{% endblock %}
 
 {% block browser_title %}{% blocktrans %}Class register: person{% endblocktrans %}{% endblock %}
 
@@ -13,13 +18,24 @@
       <i class="material-icons left">chevron_left</i> {% trans "Back" %}
     </a>
   {% endif %}
-  {% blocktrans with person=person %}
-    Class register overview for {{ person }}
-  {% endblocktrans %}
+  <span id="heading">
+    {% blocktrans with person=person %}
+      Class register overview for {{ person }}
+    {% endblocktrans %}
+  </span>
+  {% has_perm "alsijil.register_absence" user person as can_register_absence %}
+  {% if can_register_absence %}
+    <a class="btn primary-color waves-effect waves-light right" href="{% url "register_absence" person.pk %}">
+      <i class="material-icons left">rate_review</i>
+      {% trans "Register absence" %}
+    </a>
+  {% endif %}
 {% endblock %}
 
 {% block content %}
   <div class="row">
+
+  <!-- Tab Buttons -->
   <div class="col s12">
     <ul class="tabs">
       {% if register_object_table %}
@@ -30,259 +46,125 @@
       <li class="tab">
         <a href="#personal-notes">{% trans "Personal notes" %}</a>
       </li>
+      {% if stats %}
+        <li class="tab"><a href="#statistics">{% trans "Statistics" %}</a></li>
+      {% endif %}
     </ul>
   </div>
+
+  <!-- Lesson Documentation Tab -->
   {% if register_object_table %}
     <div class="col s12" id="lesson-documentations">
       {% include "alsijil/partials/objects_table.html" with table=register_object_table filter_form=filter_form %}
     </div>
   {% endif %}
-  <div class="col s12" id="personal-notes">
-    {% has_perm "alsijil.edit_person_overview_personalnote" user person as can_mark_all_as_excused %}
-    {% has_perm "alsijil.register_absence" user person as can_register_absence %}
-    {% if can_register_absence %}
-      <a class="btn primary-color waves-effect waves-light" href="{% url "register_absence" person.pk %}">
-        <i class="material-icons left">rate_review</i>
-        {% trans "Register absence" %}
-      </a>
-    {% endif %}
-
-    <div class="row">
-      <div class="col s12 m12 l6">
-        <h5>{% trans "Unexcused absences" %}</h5>
 
-        <ul class="collection">
-          {% for note in unexcused_absences %}
-            <li class="collection-item">
-              {% has_perm "alsijil.edit_personalnote" user note as can_edit_personal_note %}
-              {% if can_edit_personal_note %}
-                <form action="" method="post" class="right hide-on-small-only" style="margin-top: -7px;">
-                  {% csrf_token %}
-                  {% trans "Mark as" %}
-                  <input type="hidden" value="{{ note.pk }}" name="personal_note">
-                  {% include "alsijil/partials/mark_as_buttons.html" %}
-                  <a class="btn-flat red-text" title="{% trans "Delete note" %}"
-                     href="{% url "delete_personal_note" note.pk %}">
-                    <i class="material-icons center">cancel</i>
-                  </a>
-                </form>
-              {% endif %}
-              <i class="material-icons left red-text">warning</i>
-              <p class="no-margin">
-                <a href="{{ note.get_absolute_url }}">{{ note.date }}, {{ note.lesson_period }}</a>
-              </p>
-              {% if note.remarks %}
-                <p class="no-margin"><em>{{ note.remarks }}</em></p>
-              {% endif %}
-              {% if can_edit_personal_note %}
-                <form action="" method="post" class="hide-on-med-and-up">
-                  {% csrf_token %}
-                  {% trans "Mark as" %}
-                  <input type="hidden" value="{{ note.pk }}" name="personal_note">
-                  {% include "alsijil/partials/mark_as_buttons.html" %}
-                  <a class="btn-flat red-text" title="{% trans "Delete note" %}"
-                     href="{% url "delete_personal_note" note.pk %}">
-                    <i class="material-icons center">cancel</i>
-                  </a>
-                </form>
-              {% endif %}
-            </li>
-            {% empty %}
-            <li class="collection-item avatar valign-wrapper">
-              <i class="material-icons left materialize-circle green white-text">check</i>
-              <span class="title">{% trans "There are no unexcused lessons." %}</span>
-            </li>
-          {% endfor %}
-        </ul>
-        {% if stats %}
-          <h5>{% trans "Statistics on absences, tardiness and remarks" %}</h5>
-          <ul class="collapsible">
-            {% for school_term, stat in stats %}
-              <li {% if forloop.first %}class="active"{% endif %}>
-                <div class="collapsible-header">
-                  <i class="material-icons">date_range</i>{{ school_term }}</div>
-                <div class="collapsible-body">
-                  <table>
-                    <tr>
-                      <th colspan="2">{% trans 'Absences' %}</th>
-                      <td>{{ stat.absences_count }}</td>
-                    </tr>
-                    <tr>
-                      <td rowspan="{{ excuse_types.count|add:2 }}" class="hide-on-small-only">{% trans "thereof" %}</td>
-                      <td rowspan="{{ excuse_types.count|add:2 }}" class="hide-on-med-and-up"></td>
-                      <th class="truncate">{% trans 'Excused' %}</th>
-                      <td>{{ stat.excused }}</td>
-                    </tr>
-                    {% for excuse_type in excuse_types %}
-                      <th>{{ excuse_type.name }}</th>
-                      <td>{{ stat|get_dict:excuse_type.count_label }}</td>
-                    {% endfor %}
-                    <tr>
-                      <th>{% trans 'Unexcused' %}</th>
-                      <td>{{ stat.unexcused }}</td>
-                    </tr>
-                    <tr>
-                      <th colspan="2">{% trans 'Tardiness' %}</th>
-                      <td>{{ stat.tardiness }}'/{{ stat.tardiness_count }} &times;</td>
-                    </tr>
-                    {% for extra_mark in extra_marks %}
-                      <tr>
-                        <th colspan="2">{{ extra_mark.name }}</th>
-                        <td>{{ stat|get_dict:extra_mark.count_label }}</td>
-                      </tr>
-                    {% endfor %}
-                  </table>
-                </div>
-              </li>
-            {% endfor %}
-          </ul>
-        {% endif %}
+  <!-- Personal Note Tab -->
+  <div class="col s12" id="personal-notes">
+    <div class="col s12" id="overview">
+      <h5>{% trans "Relevant personal notes" %}</h5>
+      <form class="modal" id="filter-modal">
+        <div class="modal-content">
+          <h4>{% trans "Filter personal notes" %}</h4>
+          {% form form=personal_note_filter_form %}{% endform %}
+        </div>
+        <div class="modal-footer">
+          <button type="button" class="btn-flat secondary-color-text waves-effect waves-ripple" id="remove-filters">
+            <i class="material-icons left">clear</i>{% trans "Clear all filters" %}
+          </button>
+          <button type="button" class="modal-close btn-flat red-text waves-effect waves-ripple waves-red">
+            <i class="material-icons left">cancel</i>{% trans "Close" %}
+          </button>
+          <button type="submit" class="modal-close btn-flat primary-color-text waves-effect waves-ripple waves-light">
+            <i class="material-icons left">filter_alt</i>{% trans "Filter" %}
+          </button>
+        </div>
+      </form>
+      {% has_perm "alsijil.edit_person_overview_personalnote" user person as can_mark_all_as_excused %}
+      <div class="row">
+        <div class="col s12 m3 l5 push-m9 push-l7">
+          <button
+              class="modal-trigger btn primary-color waves-effect waves-light
+              {% if can_mark_all_as_excused %} medium-high-right {% endif %}"
+              data-target="filter-modal"
+              type="button">
+            Filter results ({{ num_filters }})<i class="material-icons right">filter_alt</i>
+          </button>
+        </div>
+        <form action="" method="post" class="">
+          {% csrf_token %}
+          <div class="col s12 m9 l7 pull-m3 pull-l5 row">
+            {% if can_mark_all_as_excused %}
+              <div class="col s12 m9">
+                {% form form=action_form %}{% endform %}
+              </div>
+              <div class="col s12 m3">
+                <button type="submit" class="btn waves-effect waves-light medium-high full-width-s">
+                  Run <i class="material-icons right">send</i>
+                </button>
+              </div>
+            {% endif %}
+          </div>
+          <div class="col s12 overflow-x-scroll">
+            {% render_table personal_notes_table %}
+          </div>
+        </form>
       </div>
-      <div class="col s12 m12 l6">
-        <h5>{% trans "Relevant personal notes" %}</h5>
-        <ul class="collapsible">
-          <li>
-            <div>
-              <ul>
-                {% for note in personal_notes %}
-                  {% ifchanged note.school_term %}</ul></div></li>
-                    <li {% if forloop.first %}class="active"{% endif %}>
-                    <div class="collapsible-header"><i
-                        class="material-icons">date_range</i>{{ note.school_term }}</div>
-                    <div class="collapsible-body">
-                    <ul class="collection">
-                  {% endifchanged %}
-
-                  {% ifchanged note.week %}
-                    <li class="collection-item">
-                      <strong>{% blocktrans with week=note.calendar_week.week %}Week {{ week }}{% endblocktrans %}</strong>
-                    </li>
-                  {% endifchanged %}
-                  {% ifchanged note.date %}
-                    <li class="collection-item">
-                      {% if can_mark_all_as_excused and note.date %}
-                        <form action="" method="post" class="right hide-on-small-only" style="margin-top: -7px;">
-                          {% csrf_token %}
-                          {% trans "Mark all as" %}
-                          <input type="hidden" value="{{ note.date|date:"Y-m-d" }}" name="date">
-                          {% include "alsijil/partials/mark_as_buttons.html" %}
-                        </form>
-                      {% endif %}
-                      <i class="material-icons left">schedule</i>
-
-                      {% if note.date %}
-                        {{ note.date }}
-                      {% else %}
-                        {{ note.register_object.date_start }}
-                        {{ note.register_object.period_from.period }}.–{{ note.register_object.date_end }}
-                        {{ note.register_object.period_to.period }}.
-                      {% endif %}
-
-                      {% if can_mark_all_as_excused and note.date %}
-                        <form action="" method="post" class="hide-on-med-and-up">
-                          {% csrf_token %}
-                          {% trans "Mark all as" %}
-                          <input type="hidden" value="{{ note.date|date:"Y-m-d" }}" name="date">
-                          {% include "alsijil/partials/mark_as_buttons.html" %}
-                        </form>
-                      {% endif %}
-                    </li>
-                  {% endifchanged %}
-
-                  <li class="collection-item">
-                    <div class="row no-margin">
-                      <div class="col s2 m1">
-                        {% if note.register_object.period %}
-                          {{ note.register_object.period.period }}.
-                        {% endif %}
-                      </div>
-
-                      <div class="col s10 m4">
-                        <i class="material-icons left">event_note</i>
-                        <a href="{{ note.get_absolute_url }}">
-                          {% if note.register_object.get_subject %}
-                            {{ note.register_object.get_subject.name }}
-                          {% else %}
-                            {% trans "Event" %} ({{ note.register_object.title }})
-                          {% endif %}<br/>
-                          {{ note.register_object.teacher_names }}
-                        </a>
-                      </div>
-
-                      <div class="col s12 m7 no-padding">
-                        {% has_perm "alsijil.edit_personalnote" user note as can_edit_personal_note %}
-                        {% if note.absent and not note.excused and can_edit_personal_note %}
-                          <form action="" method="post" class="right hide-on-small-only" style="margin-top: -7px;">
-                            {% csrf_token %}
-                            {% trans "Mark as" %}
-                            <input type="hidden" value="{{ note.pk }}" name="personal_note">
-                            {% include "alsijil/partials/mark_as_buttons.html" %}
-                            <a class="btn-flat red-text" title="{% trans "Delete note" %}"
-                               href="{% url "delete_personal_note" note.pk %}">
-                              <i class="material-icons center">cancel</i>
-                            </a>
-                          </form>
-                          {% elif can_edit_personal_note %}
-                          <a class="btn-flat red-text right hide-on-small-only" title="{% trans "Delete note" %}"
-                             href="{% url "delete_personal_note" note.pk %}">
-                            <i class="material-icons center">cancel</i>
-                          </a>
-                        {% endif %}
-
-                        {% if note.absent %}
-                          <div class="chip red white-text">
-                            {% trans 'Absent' %}
-                          </div>
-                        {% endif %}
-                        {% if note.excused %}
-                          <div class="chip green white-text">
-                            {% if note.excuse_type %}
-                              {{ note.excuse_type.name }}
-                            {% else %}
-                              {% trans 'Excused' %}
-                            {% endif %}
-                          </div>
-                        {% endif %}
-
-                        {% if note.late %}
-                          <div class="chip orange white-text">
-                            {% blocktrans with late=note.late %}{{ late }}' late{% endblocktrans %}
-                          </div>
-                        {% endif %}
-
-                        {% for extra_mark in note.extra_marks.all %}
-                          <div class="chip">{{ extra_mark.name }}</div>
-                        {% endfor %}
-
-                        <em>{{ note.remarks }}</em>
+    </div>
+  </div>
 
-                      </div>
-                      <div class="col s12 hide-on-med-and-up">
-                        {% if note.absent and not note.excused and can_edit_personal_note %}
-                          <form action="" method="post">
-                            {% csrf_token %}
-                            {% trans "Mark as" %}
-                            <input type="hidden" value="{{ note.pk }}" name="personal_note">
-                            {% include "alsijil/partials/mark_as_buttons.html" %}
-                            <a class="btn-flat red-text" title="{% trans "Delete note" %}"
-                               href="{% url "delete_personal_note" note.pk %}">
-                              <i class="material-icons center">cancel</i>
-                            </a>
-                          </form>
-                          {% elif can_edit_personal_note %}
-                          <a class="btn-flat red-text" title="{% trans "Delete note" %}"
-                             href="{% url "delete_personal_note" note.pk %}">
-                            <i class="material-icons left">cancel</i>
-                            {% trans "Delete" %}
-                          </a>
-                        {% endif %}
-                      </div>
-                  </li>
+  <!-- Statistics Tab -->
+  {% if stats %}
+    <div class="col s12" id="statistics">
+      <h5>{% trans "Statistics on absences, tardiness and remarks" %}</h5>
+      <ul class="collapsible">
+        {% for school_term, stat in stats %}
+          <li {% if forloop.first %}class="active"{% endif %}>
+            <div class="collapsible-header">
+              <i class="material-icons">date_range</i>{{ school_term }}</div>
+            <div class="collapsible-body">
+              <table>
+                <tr>
+                  <th colspan="2">{% trans 'Absences' %}</th>
+                  <td>{{ stat.absences_count }}</td>
+                </tr>
+                <tr>
+                  <td rowspan="{{ excuse_types.count|add:2 }}" class="hide-on-small-only">{% trans "thereof" %}</td>
+                  <td rowspan="{{ excuse_types.count|add:2 }}" class="hide-on-med-and-up"></td>
+                  <th class="truncate">{% trans 'Excused' %}</th>
+                  <td>{{ stat.excused }}</td>
+                </tr>
+                {% for excuse_type in excuse_types %}
+                  <th>{{ excuse_type.name }}</th>
+                  <td>{{ stat|get_dict:excuse_type.count_label }}</td>
                 {% endfor %}
-                </li>
-                </ul>
-                </div>
-      </div>
+                <tr>
+                  <th>{% trans 'Unexcused' %}</th>
+                  <td>{{ stat.unexcused }}</td>
+                </tr>
+                <tr>
+                  <th colspan="2">{% trans 'Tardiness' %}</th>
+                  <td>{{ stat.tardiness }}'/{{ stat.tardiness_count }} &times;</td>
+                </tr>
+                {% for extra_mark in extra_marks %}
+                  <tr>
+                    <th colspan="2">{{ extra_mark.name }}</th>
+                    <td>{{ stat|get_dict:extra_mark.count_label }}</td>
+                  </tr>
+                {% endfor %}
+              </table>
+            </div>
+          </li>
+        {% endfor %}
+      </ul>
     </div>
-  </div>
+  {% endif %}
+  <script type="text/javascript">
+    $("#remove-filters").click(function () {
+      $("#filter-modal").trigger("reset");
+      $("#filter-modal input, #filter-modal select").each(function () {
+        $(this).val("");
+      })
+    })
+  </script>
 {% endblock %}
diff --git a/aleksis/apps/alsijil/templates/alsijil/partials/mark_as_buttons.html b/aleksis/apps/alsijil/templates/alsijil/partials/mark_as_buttons.html
deleted file mode 100644
index 5b198afa4bceea55c2b749fa8b3f3b8d88b335e8..0000000000000000000000000000000000000000
--- a/aleksis/apps/alsijil/templates/alsijil/partials/mark_as_buttons.html
+++ /dev/null
@@ -1,12 +0,0 @@
-{% load i18n %}
-<button type="submit" class="btn-flat tooltipped" name="excuse_type" value="e" title="{% trans "Excused" %}"
-        data-position="bottom" data-tooltip="{% trans "Excused" %}" style="width: 50px;">
-  {% trans "e" %}
-</button>
-{% for excuse_type in excuse_types %}
-  <button type="submit" class="btn-flat tooltipped" value="{{ excuse_type.pk }}" name="excuse_type"
-          title="{{ excuse_type.name }}" data-position="bottom" data-tooltip="{{ excuse_type.name }}"
-          style="width: 50px;">
-    {{ excuse_type.short_name }}
-  </button>
-{% endfor %}
diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py
index c4c4385f6e00bafe70e7c0147e0e974c9132d5d9..42237abc840ebd246a6a249168fc20f2d9667aa9 100644
--- a/aleksis/apps/alsijil/views.py
+++ b/aleksis/apps/alsijil/views.py
@@ -42,6 +42,7 @@ from aleksis.core.util.core_helpers import get_site_preferences, objectgetter_op
 from aleksis.core.util.pdf import render_pdf
 from aleksis.core.util.predicates import check_global_permission
 
+from .filters import PersonalNoteFilter
 from .forms import (
     AssignGroupRoleForm,
     ExcuseTypeForm,
@@ -51,6 +52,7 @@ from .forms import (
     GroupRoleForm,
     LessonDocumentationForm,
     PersonalNoteFormSet,
+    PersonOverviewForm,
     RegisterAbsenceForm,
     RegisterObjectActionForm,
     SelectForm,
@@ -67,6 +69,7 @@ from .tables import (
     ExcuseTypeTable,
     ExtraMarkTable,
     GroupRoleTable,
+    PersonalNoteTable,
     RegisterObjectSelectTable,
     RegisterObjectTable,
 )
@@ -772,76 +775,14 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp
     )(request, id_)
     context["person"] = person
 
-    if request.method == "POST":
-        if request.POST.get("excuse_type"):
-            # Get excuse type
-            excuse_type = request.POST["excuse_type"]
-            found = False
-            if excuse_type == "e":
-                excuse_type = None
-                found = True
-            else:
-                try:
-                    excuse_type = ExcuseType.objects.get(pk=int(excuse_type))
-                    found = True
-                except (ExcuseType.DoesNotExist, ValueError):
-                    pass
-
-            if found:
-                if request.POST.get("date"):
-                    # Mark absences on date as excused
-                    try:
-                        date = datetime.strptime(request.POST["date"], "%Y-%m-%d").date()
-
-                        if not request.user.has_perm(
-                            "alsijil.edit_person_overview_personalnote", person
-                        ):
-                            raise PermissionDenied()
-
-                        notes = person.personal_notes.filter(absent=True, excused=False,).filter(
-                            Q(
-                                week=date.isocalendar()[1],
-                                lesson_period__period__weekday=date.weekday(),
-                                lesson_period__lesson__validity__date_start__lte=date,
-                                lesson_period__lesson__validity__date_end__gte=date,
-                            )
-                            | Q(
-                                extra_lesson__week=date.isocalendar()[1],
-                                extra_lesson__period__weekday=date.weekday(),
-                            )
-                        )
-                        for note in notes:
-                            note.excused = True
-                            note.excuse_type = excuse_type
-                            with reversion.create_revision():
-                                reversion.set_user(request.user)
-                                note.save()
-
-                        messages.success(request, _("The absences have been marked as excused."))
-                    except ValueError:
-                        pass
-                elif request.POST.get("personal_note"):
-                    # Mark specific absence as excused
-                    try:
-                        note = PersonalNote.objects.get(pk=int(request.POST["personal_note"]))
-                        if not request.user.has_perm("alsijil.edit_personalnote", note):
-                            raise PermissionDenied()
-                        if note.absent:
-                            note.excused = True
-                            note.excuse_type = excuse_type
-                            with reversion.create_revision():
-                                reversion.set_user(request.user)
-                                note.save()
-                            messages.success(request, _("The absence has been marked as excused."))
-                    except (PersonalNote.DoesNotExist, ValueError):
-                        pass
-
-                person.refresh_from_db()
-
-    person_personal_notes = person.personal_notes.all().prefetch_related(
-        "lesson_period__lesson__groups",
-        "lesson_period__lesson__teachers",
-        "lesson_period__substitutions",
+    person_personal_notes = (
+        person.personal_notes.all()
+        .prefetch_related(
+            "lesson_period__lesson__groups",
+            "lesson_period__lesson__teachers",
+            "lesson_period__substitutions",
+        )
+        .annotate_date_range()
     )
 
     # Prefetch object permissions for groups the person is a member of
@@ -896,11 +837,33 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp
                 When(extra_lesson__isnull=False, then="extra_lesson__period__period"),
                 When(lesson_period__isnull=False, then="lesson_period__period__period"),
             ),
+            order_groups=Case(
+                When(event__isnull=False, then="event__groups"),
+                When(extra_lesson__isnull=False, then="extra_lesson__groups"),
+                When(lesson_period__isnull=False, then="lesson_period__lesson__groups"),
+            ),
+            order_teachers=Case(
+                When(event__isnull=False, then="event__teachers"),
+                When(extra_lesson__isnull=False, then="extra_lesson__teachers"),
+                When(lesson_period__isnull=False, then="lesson_period__lesson__teachers"),
+            ),
         )
         .order_by(
             "-school_term_start", "-order_year", "-order_week", "-order_weekday", "order_period",
         )
+        .annotate_date_range()
+        .annotate_subject()
     )
+
+    personal_note_filter_object = PersonalNoteFilter(request.GET, queryset=personal_notes)
+    filtered_personal_notes = personal_note_filter_object.qs
+    context["personal_note_filter_form"] = personal_note_filter_object.form
+
+    used_filters = list(personal_note_filter_object.data.values())
+    context["num_filters"] = (
+        len(used_filters) - used_filters.count("") - used_filters.count("unknown")
+    )
+
     personal_notes_list = []
     for note in personal_notes:
         note.set_object_permission_checker(checker)
@@ -908,6 +871,19 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp
     context["personal_notes"] = personal_notes_list
     context["excuse_types"] = ExcuseType.objects.all()
 
+    form = PersonOverviewForm(request, request.POST or None, queryset=allowed_personal_notes)
+    if request.method == "POST":
+        if form.is_valid():
+            with reversion.create_revision():
+                reversion.set_user(request.user)
+                form.execute()
+            person.refresh_from_db()
+    context["action_form"] = form
+
+    table = PersonalNoteTable(filtered_personal_notes)
+    RequestConfig(request, paginate={"per_page": 20}).configure(table)
+    context["personal_notes_table"] = table
+
     extra_marks = ExtraMark.objects.all()
     excuse_types = ExcuseType.objects.all()
     if request.user.has_perm("alsijil.view_person_statistics_personalnote", person):