diff --git a/aleksis/apps/chronos/migrations/0010_absence_reason_name.py b/aleksis/apps/chronos/migrations/0010_absence_reason_name.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c57327a2e19c8ff2865d14fabcd65acd317146f
--- /dev/null
+++ b/aleksis/apps/chronos/migrations/0010_absence_reason_name.py
@@ -0,0 +1,33 @@
+# Generated by Django 3.0.5 on 2020-04-13 13:36
+
+from django.db import migrations, models
+from django.db.models import F
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('chronos', '0009_extended_data'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='absencereason',
+            name='name',
+            field=models.CharField(default=F("description"), blank=True, max_length=255, null=True, verbose_name='Name'),
+        ),
+        migrations.AddField(
+            model_name='absencereason',
+            name='short_name',
+            field=models.CharField(default=F("title"), max_length=255, verbose_name='Short name'),
+            preserve_default=False,
+        ),
+        migrations.RemoveField(
+            model_name='absencereason',
+            name='description',
+        ),
+        migrations.RemoveField(
+            model_name='absencereason',
+            name='title',
+        ),
+    ]
diff --git a/aleksis/apps/chronos/migrations/0011_absence_for_groups_and_rooms.py b/aleksis/apps/chronos/migrations/0011_absence_for_groups_and_rooms.py
new file mode 100644
index 0000000000000000000000000000000000000000..008a723af7c68f96dbff9f8b149fb3c36305028e
--- /dev/null
+++ b/aleksis/apps/chronos/migrations/0011_absence_for_groups_and_rooms.py
@@ -0,0 +1,33 @@
+# Generated by Django 3.0.5 on 2020-04-13 15:07
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('chronos', '0010_absence_reason_name'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='absence',
+            name='person',
+        ),
+        migrations.AddField(
+            model_name='absence',
+            name='group',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='absences', to='core.Group'),
+        ),
+        migrations.AddField(
+            model_name='absence',
+            name='room',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='absences', to='chronos.Room'),
+        ),
+        migrations.AddField(
+            model_name='absence',
+            name='teacher',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='absences', to='core.Person'),
+        ),
+    ]
diff --git a/aleksis/apps/chronos/migrations/0012_event_remove_absence_reason.py b/aleksis/apps/chronos/migrations/0012_event_remove_absence_reason.py
new file mode 100644
index 0000000000000000000000000000000000000000..4e6611963aab5f79179b0489ca4bd41d25bd4433
--- /dev/null
+++ b/aleksis/apps/chronos/migrations/0012_event_remove_absence_reason.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.0.5 on 2020-04-13 16:07
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('chronos', '0011_absence_for_groups_and_rooms'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='event',
+            name='absence_reason',
+        ),
+    ]
diff --git a/aleksis/apps/chronos/migrations/0013_event_title_optional.py b/aleksis/apps/chronos/migrations/0013_event_title_optional.py
new file mode 100644
index 0000000000000000000000000000000000000000..4bc05c39f8307ca056d03102c8709c018a432f63
--- /dev/null
+++ b/aleksis/apps/chronos/migrations/0013_event_title_optional.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.5 on 2020-04-14 16:11
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('chronos', '0012_event_remove_absence_reason'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='event',
+            name='title',
+            field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Title'),
+        ),
+    ]
diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py
index 9e9a6418ba117dcdf5b1ec4603e5d7515f35b674..016204bc19a5beb6f67311bc3c419e4097dee8ef 100644
--- a/aleksis/apps/chronos/models.py
+++ b/aleksis/apps/chronos/models.py
@@ -4,6 +4,7 @@ from collections import OrderedDict
 from datetime import date, datetime, timedelta, time
 from typing import Dict, Optional, Tuple, Union
 
+from constance import config
 from django.core import validators
 from django.core.exceptions import ValidationError
 from django.db import models
@@ -142,18 +143,18 @@ class LessonDataQuerySet(models.QuerySet):
     def filter_teacher(self, teacher: Union[Person, int]):
         """ Filter for all lessons given by a certain teacher. """
 
-        return self.filter(
-            Q(**{self._subst_path + "teachers": teacher, self._subst_path + "week": F("_week"),})
-            | Q(**{self._period_path + "lesson__teachers": teacher})
-        )
+        qs1 = self.filter(**{self._period_path + "lesson__teachers": teacher})
+        qs2 = self.filter(**{self._subst_path + "teachers": teacher, self._subst_path + "week": F("_week"), })
+
+        return qs1.union(qs2)
 
     def filter_room(self, room: Union[Room, int]):
         """ Filter for all lessons taking part in a certain room. """
 
-        return self.filter(
-            Q(**{self._subst_path + "room": room, self._subst_path + "week": F("_week"),})
-            | Q(**{self._period_path + "room": room})
-        )
+        qs1 = self.filter(**{self._period_path + "room": room})
+        qs2 = self.filter(**{self._subst_path + "room": room, self._subst_path + "week": F("_week"),})
+
+        return qs1.union(qs2)
 
     def annotate_week(self, week: Union[CalendarWeek, int]):
         """ Annotate all lessons in the QuerySet with the number of the provided calendar week. """
@@ -221,24 +222,24 @@ class LessonPeriodQuerySet(LessonDataQuerySet):
         if type_ == "teacher":
             # Teacher
 
-            return person.lesson_periods_as_teacher
+            return self.filter_teacher(person)
 
         elif type_ == "group":
             # Student
 
-            return person.lesson_periods_as_participant
+            return self.filter(lesson__groups__members=person)
 
         else:
             # If no student or teacher
             return None
 
     def daily_lessons_for_person(self, person: Person, wanted_day: date) -> Optional[models.QuerySet]:
-        lesson_periods = LessonPeriod.objects.filter_from_person(person)
-
-        if lesson_periods is None:
+        if person.timetable_type is None:
             return None
 
-        return lesson_periods.on_day(wanted_day)
+        lesson_periods = self.on_day(wanted_day).filter_from_person(person)
+
+        return lesson_periods
 
     def per_period_one_day(self) -> OrderedDict:
         """ Group selected lessons per period for one day """
@@ -442,6 +443,18 @@ class Lesson(ExtensibleModel):
     def group_names(self, sep: Optional[str] = ", ") -> str:
         return sep.join([group.short_name for group in self.groups.all()])
 
+    @property
+    def groups_to_show(self) -> models.QuerySet:
+        groups = self.groups.all()
+        if groups.count() == 1 and groups[0].parent_groups.all() and config.CHRONOS_USE_PARENT_GROUPS:
+            return groups[0].parent_groups.all()
+        else:
+            return groups
+
+    @property
+    def groups_to_show_names(self, sep: Optional[str] = ", ") -> str:
+        return sep.join([group.short_name for group in self.groups_to_show])
+
     def get_calendar_week(self, week: int):
         year = self.date_start.year
         if week < int(self.date_start.strftime("%V")):
@@ -485,9 +498,10 @@ class LessonSubstitution(ExtensibleModel):
 
     @property
     def type_(self):
-        # TODO: Add cases events and supervisions
         if self.cancelled:
             return "cancellation"
+        elif self.cancelled_for_teachers:
+            return "cancellation_for_teachers"
         else:
             return "substitution"
 
@@ -508,6 +522,8 @@ class LessonSubstitution(ExtensibleModel):
 
 
 class LessonPeriod(ExtensibleModel):
+    label_ = "lesson_period"
+
     objects = LessonPeriodManager.from_queryset(LessonPeriodQuerySet)()
 
     lesson = models.ForeignKey("Lesson", models.CASCADE, related_name="lesson_periods")
@@ -567,21 +583,25 @@ class TimetableWidget(DashboardWidget):
     template = "chronos/widget.html"
 
     def get_context(self):
+        from aleksis.apps.chronos.util.build import build_timetable # noqa
+
         request = get_request()
         context = {"has_plan": True}
         wanted_day = TimePeriod.get_next_relevant_day(timezone.now().date(), datetime.now().time())
 
         if has_person(request.user):
             person = request.user.person
-
-            lesson_periods = LessonPeriod.objects.daily_lessons_for_person(person, wanted_day)
             type_ = person.timetable_type
 
+            # Build timetable
+            timetable = build_timetable("person", person, wanted_day)
+
             if type_ is None:
                 # If no student or teacher, redirect to all timetables
                 context["has_plan"] = False
             else:
-                context["lesson_periods"] = lesson_periods.per_period_one_day()
+                context["timetable"] = timetable
+                context["holiday"] = Holiday.on_day(wanted_day)
                 context["type"] = type_
                 context["day"] = wanted_day
                 context["periods"] = TimePeriod.get_times_dict()
@@ -600,9 +620,36 @@ class TimetableWidget(DashboardWidget):
         verbose_name = _("Timetable widget")
 
 
+class DateRangeQuerySet(models.QuerySet):
+    def within_dates(self, start: date, end: date):
+        """ Filter for all events within a date range. """
+
+        return self.filter(date_start__lte=end, date_end__gte=start)
+
+    def in_week(self, wanted_week: CalendarWeek):
+        """ Filter for all events within a calendar week. """
+
+        return self.within_dates(wanted_week[0], wanted_week[6])
+
+    def on_day(self, day: date):
+        """ Filter for all events on a certain day. """
+
+        return self.within_dates(day, day)
+
+    def at_time(self, when: Optional[datetime] = None):
+        """ Filter for the events taking place at a certain point in time. """
+
+        now = when or datetime.now()
+
+        return self.on_day(now.date()).filter(
+            period_from__time_start__lte=now.time(),
+            period_to__time_end__gte=now.time()
+        )
+
+
 class AbsenceReason(ExtensibleModel):
-    title = models.CharField(verbose_name=_("Title"), max_length=50)
-    description = models.TextField(verbose_name=_("Description"), blank=True, null=True)
+    short_name = models.CharField(verbose_name=_("Short name"), max_length=255)
+    name = models.CharField(verbose_name=_("Name"), blank=True, null=True, max_length=255)
 
     class Meta:
         verbose_name = _("Absence reason")
@@ -610,7 +657,10 @@ class AbsenceReason(ExtensibleModel):
 
 class Absence(ExtensibleModel):
     reason = models.ForeignKey("AbsenceReason", on_delete=models.CASCADE, related_name="absences")
-    person = models.ManyToManyField("core.Person", related_name="absences")
+
+    teacher = models.ForeignKey("core.Person", on_delete=models.CASCADE, related_name="absences", null=True, blank=True)
+    group = models.ForeignKey("core.Group", on_delete=models.CASCADE, related_name="absences", null=True, blank=True)
+    room = models.ForeignKey("Room", on_delete=models.CASCADE, related_name="absences", null=True, blank=True)
 
     date_start = models.DateField(verbose_name=_("Effective start date of absence"), null=True)
     date_end = models.DateField(verbose_name=_("Effective end date of absence"), null=True)
@@ -642,12 +692,38 @@ class Exam(ExtensibleModel):
         verbose_name_plural = _("Exams")
 
 
+class HolidayQuerySet(DateRangeQuerySet):
+    pass
+
+
 class Holiday(ExtensibleModel):
+    objects = models.Manager.from_queryset(HolidayQuerySet)()
+
     title = models.CharField(verbose_name=_("Title of the holidays"), max_length=50)
     date_start = models.DateField(verbose_name=_("Effective start date of holidays"), null=True)
     date_end = models.DateField(verbose_name=_("Effective end date of holidays"), null=True)
     comments = models.TextField(verbose_name=_("Comments"), null=True, blank=True)
 
+    @classmethod
+    def on_day(cls, day: date) -> Optional["Holiday"]:
+        holidays = cls.objects.on_day(day)
+        if holidays.exists():
+            return holidays[0]
+        else:
+            return None
+
+    @classmethod
+    def in_week(cls, week: CalendarWeek) -> Dict[int, Optional["Holiday"]]:
+        per_weekday = {}
+
+        for weekday in range(TimePeriod.weekday_min, TimePeriod.weekday_max + 1):
+            holiday_date = week[weekday]
+            holidays = Holiday.objects.on_day(holiday_date)
+            if holidays.exists():
+                per_weekday[weekday] = holidays[0]
+
+        return per_weekday
+
     class Meta:
         ordering = ["date_start"]
         indexes = [models.Index(fields=["date_start", "date_end"])]
@@ -686,6 +762,14 @@ class Break(ExtensibleModel):
             else self.before_period.weekday
         )
 
+    @property
+    def after_period_number(self):
+        return (
+            self.after_period.period
+            if self.after_period
+            else self.before_period.period - 1
+        )
+
     @property
     def before_period_number(self):
         return (
@@ -694,6 +778,22 @@ class Break(ExtensibleModel):
             else self.after_period.period + 1
         )
 
+    @property
+    def time_start(self):
+        return self.after_period.time_end if self.after_period else None
+
+    @property
+    def time_end(self):
+        return self.before_period.time_start if self.before_period else None
+
+    @classmethod
+    def get_breaks_dict(cls) -> Dict[int, Tuple[datetime, datetime]]:
+        breaks = {}
+        for break_ in cls.objects.all():
+            breaks[break_.before_period_number] = break_
+
+        return breaks
+
     class Meta:
         ordering = ["after_period"]
         indexes = [models.Index(fields=["after_period", "before_period"])]
@@ -701,11 +801,64 @@ class Break(ExtensibleModel):
         verbose_name_plural = _("Breaks")
 
 
+class SupervisionQuerySet(models.QuerySet):
+    def annotate_week(self, week: Union[CalendarWeek, int]):
+        """ Annotate all lessons in the QuerySet with the number of the provided calendar week. """
+
+        if isinstance(week, CalendarWeek):
+            week_num = week.week
+        else:
+            week_num = week
+
+        return self.annotate(_week=models.Value(week_num, models.IntegerField()))
+
+    def filter_by_weekday(self, weekday: int):
+        self.filter(
+            Q(break_item__before_period__weekday=weekday)
+            | Q(break_item__after_period__weekday=weekday)
+        )
+
+    def filter_by_teacher(self, teacher: Union[Person, int]):
+        """ Filter for all supervisions given by a certain teacher. """
+
+        if self.count() > 0:
+            if hasattr(self[0], "_week"):
+                week = CalendarWeek(week=self[0]._week)
+            else:
+                week = CalendarWeek.current_week()
+
+            dates = [week[w] for w in range(0, 7)]
+
+            return self.filter(Q(substitutions__teacher=teacher, substitutions__date__in=dates) | Q(teacher=teacher))
+
+        return self
+
+
 class Supervision(ExtensibleModel):
+    objects = models.Manager.from_queryset(SupervisionQuerySet)()
+
     area = models.ForeignKey(SupervisionArea, models.CASCADE, verbose_name=_("Supervision area"), related_name="supervisions")
     break_item = models.ForeignKey(Break, models.CASCADE, verbose_name=_("Break"), related_name="supervisions")
     teacher = models.ForeignKey("core.Person", models.CASCADE, related_name="supervisions", verbose_name=_("Teacher"))
 
+    def get_substitution(
+        self, week: Optional[int] = None
+    ) -> Optional[SupervisionSubstitution]:
+        wanted_week = week or getattr(self, "_week", None) or CalendarWeek().week
+        wanted_week = CalendarWeek(week=wanted_week)
+        # We iterate over all substitutions because this can make use of
+        # prefetching when this model is loaded from outside, in contrast
+        # to .filter()
+        for substitution in self.substitutions.all():
+            for weekday in range(0, 7):
+                if substitution.date == wanted_week[weekday]:
+                    return substitution
+        return None
+
+    @property
+    def teachers(self):
+        return [self.teacher]
+
     class Meta:
         ordering = ["area", "break_item"]
         verbose_name= _("Supervision")
@@ -717,17 +870,82 @@ class SupervisionSubstitution(ExtensibleModel):
     supervision = models.ForeignKey(Supervision, models.CASCADE, verbose_name=_("Supervision"), related_name="substitutions")
     teacher = models.ForeignKey("core.Person", models.CASCADE, related_name="substituted_supervisions", verbose_name=_("Teacher"))
 
+    @property
+    def teachers(self):
+        return [self.teacher]
+
     class Meta:
         ordering = ["date", "supervision"]
         verbose_name = _("Supervision substitution")
         verbose_name_plural = _("Supervision substitutions")
 
 
+class EventQuerySet(DateRangeQuerySet):
+    def filter_participant(self, person: Union[Person, int]):
+        """ Filter for all lessons a participant (student) attends. """
+
+        return self.filter(Q(groups_members=person))
+
+    def filter_group(self, group: Union[Group, int]):
+        """ Filter for all events a group (class) attends. """
+
+        if isinstance(group, int):
+            group = Group.objects.get(pk=group)
+
+        if group.parent_groups.all():
+            # Prevent to show lessons multiple times
+            return self.filter(groups=group)
+        else:
+            return self.filter(Q(groups=group) | Q(groups__parent_groups=group))
+
+    def filter_teacher(self, teacher: Union[Person, int]):
+        """ Filter for all lessons given by a certain teacher. """
+
+        return self.filter(teachers=teacher)
+
+    def filter_room(self, room: Union[Room, int]):
+        """ Filter for all lessons taking part in a certain room. """
+
+        return self.filter(rooms=room)
+
+    def filter_from_type(self, type_: str, pk: int) -> Optional[models.QuerySet]:
+        if type_ == "group":
+            return self.filter_group(pk)
+        elif type_ == "teacher":
+            return self.filter_teacher(pk)
+        elif type_ == "room":
+            return self.filter_room(pk)
+        else:
+            return None
+
+    def filter_from_person(self, person: Person) -> Optional[models.QuerySet]:
+        type_ = person.timetable_type
+
+        if type_ == "teacher":
+            # Teacher
+
+            return self.filter_teacher(person)
+
+        elif type_ == "group":
+            # Student
+
+            return self.filter_participant(person)
+
+        else:
+            # If no student or teacher
+            return None
+
+
 class Event(ExtensibleModel):
-    title = models.CharField(verbose_name=_("Title"), max_length=50)
+    label_ = "event"
+
+    objects = models.Manager.from_queryset(EventQuerySet)()
+
+    title = models.CharField(verbose_name=_("Title"), max_length=255, blank=True, null=True)
+
     date_start = models.DateField(verbose_name=_("Effective start date of event"), null=True)
     date_end = models.DateField(verbose_name=_("Effective end date of event"), null=True)
-    absence_reason = models.ForeignKey("AbsenceReason", on_delete=models.CASCADE, related_name="absence_reason", verbose_name=_("Absence reason"))
+
     period_from = models.ForeignKey("TimePeriod", on_delete=models.CASCADE, verbose_name=_("Effective start period of event"), related_name="+")
     period_to = models.ForeignKey("TimePeriod", on_delete=models.CASCADE, verbose_name=_("Effective end period of event"), related_name="+")
 
diff --git a/aleksis/apps/chronos/static/css/chronos/timetable.css b/aleksis/apps/chronos/static/css/chronos/timetable.css
index c25d6750eeed8f9965c06b3f6da267916ee4c616..bbdbb5b82c664ca908c2cd2088188c0a64395716 100644
--- a/aleksis/apps/chronos/static/css/chronos/timetable.css
+++ b/aleksis/apps/chronos/static/css/chronos/timetable.css
@@ -26,6 +26,10 @@ li.active > a > .sidenav-badge {
     min-height: 65px;
 }
 
+.supervision-card {
+    min-height: 35px;
+}
+
 .lesson-card .card-content {
     padding: 0;
     text-align: center;
@@ -53,6 +57,8 @@ li.active > a > .sidenav-badge {
 .timetable-mobile-title-card {
     margin-top: 50px;
     margin-bottom: .60rem;
+    display: flex;
+    flex-grow: 1;
 }
 
 .timetable-mobile-title-card:first-child {
@@ -112,3 +118,23 @@ table.substitutions td, table.substitutions th {
 .black-text-a a {
     color: black;
 }
+
+.holiday-badge {
+    float: left !important;
+    position: relative;
+    margin-left: 0 !important;
+    left: 50%;
+    transform: translate(-50%);
+    width: auto;
+    height: auto !important;
+    min-height: 26px;
+    margin-top: 5px;
+}
+
+.holiday-card .card-content{
+    width: 100%;
+}
+
+.holiday-card .holiday-badge {
+    margin-top: 0;
+}
diff --git a/aleksis/apps/chronos/templates/chronos/partials/elements.html b/aleksis/apps/chronos/templates/chronos/partials/elements.html
new file mode 100644
index 0000000000000000000000000000000000000000..a2e55aeceaf612741c6a9520d90e4b025014be92
--- /dev/null
+++ b/aleksis/apps/chronos/templates/chronos/partials/elements.html
@@ -0,0 +1,11 @@
+<div class="card lesson-card">
+  <div class="card-content">
+    {% for element in elements %}
+      {% if element.label_ == "lesson_period" %}
+        {% include "chronos/partials/lesson.html" with lesson_period=element %}
+      {% elif element.label_ == "event" %}
+        {% include "chronos/partials/event.html" with event=element %}
+      {% endif %}
+    {% endfor %}
+  </div>
+</div>
diff --git a/aleksis/apps/chronos/templates/chronos/partials/event.html b/aleksis/apps/chronos/templates/chronos/partials/event.html
new file mode 100644
index 0000000000000000000000000000000000000000..29b025ed48f4d84acef6094dea0b5b0ec95a6e5d
--- /dev/null
+++ b/aleksis/apps/chronos/templates/chronos/partials/event.html
@@ -0,0 +1,37 @@
+<div class="lesson-with-event">
+  <p>
+    {# Teacher or room > Display groups #}
+    {% if type == "teacher" or type == "room" %}
+      {% include "chronos/partials/groups.html" with groups=event.groups.all %}
+    {% endif %}
+
+    {# Class or room > Display teachers #}
+    {% if type == "room" or type == "group" %}
+      {% include "chronos/partials/teachers.html" with teachers=event.teachers.all %}
+    {% endif %}
+
+    {# Teacher or class > Display rooms #}
+    {% if type == "teacher" or type == "group" %}
+      {% for room in event.rooms.all %}
+        <span class="tooltipped" data-position="bottom" data-tooltip="{{ room.name }}">
+          <a href="{% url "timetable" "room" room.pk %}">
+            {{ room.short_name }}
+          </a>
+        </span>
+      {% endfor %}
+    {% endif %}
+
+    {% if type == "teacher" and not event.groups.all and not event.rooms.all and event.title %}
+      <em>{{ event.title }}</em>
+    {% elif type == "group" and not event.teachers.all and not event.groups.all and event.title %}
+      <em>{{ event.title }}</em>
+    {% elif type == "room" and not event.teachers.all and not event.groups.all and event.title %}
+      <em>{{ event.title }}</em>
+    {% elif event.title %}
+      <br/>
+      <small>
+        <em>{{ event.title }}</em>
+      </small>
+    {% endif %}
+  </p>
+</div>
diff --git a/aleksis/apps/chronos/templates/chronos/partials/holiday.html b/aleksis/apps/chronos/templates/chronos/partials/holiday.html
new file mode 100644
index 0000000000000000000000000000000000000000..85fc42d4ffd50621da8fe04802b3c2d18a509b71
--- /dev/null
+++ b/aleksis/apps/chronos/templates/chronos/partials/holiday.html
@@ -0,0 +1 @@
+<span class="badge new blue center-align holiday-badge">{{ holiday.title }}</span>
diff --git a/aleksis/apps/chronos/templates/chronos/partials/lesson.html b/aleksis/apps/chronos/templates/chronos/partials/lesson.html
index 8dc4f9fb2f264cfe188241502d298cf3e095ccf3..3eef028d830f3842181c9ccb70dc660488b53981 100644
--- a/aleksis/apps/chronos/templates/chronos/partials/lesson.html
+++ b/aleksis/apps/chronos/templates/chronos/partials/lesson.html
@@ -1,131 +1,105 @@
 {% load i18n %}
 
-<div class="card lesson-card">
-  <div class="card-content">
-
-    {# Every element of the lesson #}
-    {% for lesson_period in lessons %}
-      <div style="
-              {# Display background color only if no badge exists and it is not the old room and there are no holidays #}
-              {% if not lesson_period.get_substitution.cancelled and not lesson_period.is_hol %}
-                {% if not lesson_period.room != lesson_period.get_room or type != 1 %}
-                  {% if lesson_period.lesson.subject.colour_fg %}
-                    color: {{ lesson_period.lesson.subject.colour_fg }};
-                  {% endif %}
-                  {% if lesson_period.lesson.subject.colour_bg %}
-                    background-color: {{ lesson_period.lesson.subject.colour_bg }};
-                  {% endif %}
-                {% endif %}
-              {% endif %}
-              "
-              {# Add CSS class for sub when it's a sub #}
-           class="{% if lesson_period.get_substitution and smart %}{% if lesson_period.substitution.table.is_event %}lesson-with-event{% else %}lesson-with-sub{% endif %}{% endif %}"
-      >
-        <p>
-          {% if lesson_period.is_hol and smart %}
-            {# Do nothing #}
-          {% elif lesson_period.get_substitution and smart %}
-            {% with sub=lesson_period.get_substitution %}
-              {# SUBSTITUTION #}
-              {% if type == "room" and lesson_period.room != lesson_period.get_room %}
-                {# When it's the old room, let it empty #}
-
-              {% elif lesson_period.get_substitution.cancelled %}
-                {# When a badge (cancellation, etc.) exists, then display it with the teacher#}
-
-                {# Class or room > Display teacher #}
-                {% if type == "group" or type == "room" and lesson_period.lesson.teachers.all %}
-                  {% include "chronos/partials/teachers.html" with teachers=lesson_period.lesson.teachers.all %}<br/>
-                {% endif %}
-
-                {# Badge #}
-                <span class="badge new green darken-2">{% trans "Cancelled" %}</span>
-
-              {% else %}
-                {# Display sub #}
-
-                {# Teacher or room > display classes #}
-                {% if type == "teacher" or type == "room" %}
-                  {% include "chronos/partials/groups.html" with groups=lesson_period.lesson.groups.all %}
-                {% endif %}
-
-                {# Display teacher with tooltip #}
-                {% include "chronos/partials/subs/teachers.html" %}
-
-                {# Display subject #}
-                {% include "chronos/partials/subs/subject.html" %}
-
-                {# Teacher or class > display room #}
-                {% if type == "teacher" or type == "group" %}
-                  {% include "chronos/partials/subs/room.html" %}
-                {% endif %}
-              {% endif %}
-
-
-              {# When it isn't a room or the old plan, then display the extra text (e. g. work orders) AND NOT A EVENT#}
-              {% if not lesson_period.substitution.table.is_event %}
-                {% if not type == "room"  or not lesson_period.is_old %}
-                  <br>
-                  <small>
-                    <em>{{ lesson_period.substitution.table.text|default:"" }}</em>
-                  </small>
-                {% endif %}
-              {% endif %}
-              {# Display the extra text for events #}
-              {% if lesson_period.substitution.table.is_event %}
-                {% if type == 0 and lesson_period.substitution.table.classes == "" and lesson_period.substitution.table.rooms|length == 0 and lesson_period.substitutions.table.teachers|length == 0 %}
-                  <em>{{ lesson_period.substitution.table.text|default:"" }}</em>
-                {% elif type == 2 and  lesson_period.substitution.table.teachers|length == 0  and  lesson_period.substitution.table.rooms|length == 0 %}
-                  <em>{{ lesson_period.substitution.table.text|default:"" }}</em>
-                {% elif type == 1  and  lesson_period.substitution.table.teachers|length == 0 and lesson_period.substitution.table.classes == "" %}
-                  <em>{{ lesson_period.substitution.table.text|default:"" }}</em>
-                {% else %}
-                  <br>
-                  <small>
-                    <em>{{ lesson_period.substitution.table.text|default:"" }}</em>
-                  </small>
-                {% endif %}
-              {% endif %}
-            {% endwith %}
-
-
-          {% else %}
-            {# Normal plan #}
-
-            {# Teacher or room > Display classes #}
-            {% if type == "teacher" or type == "room" %}
-              {#                            {{ element_container.element.classes }}#}
-              {% if lesson_period.lesson.groups %}
-                {% include "chronos/partials/groups.html" with groups=lesson_period.lesson.groups.all %}
-              {% endif %}
+<div style="
+        {# Display background color only if no badge exists and it is not the old room and there are no holidays #}
+        {% if not lesson_period.get_substitution.cancelled and not lesson_period.get_substitution.cancelled_for_teachers %}
+          {% if not type == "room" or lesson_period.room == lesson_period.get_room or lesson_period.get_room == el %}
+            {% if lesson_period.lesson.subject.colour_fg %}
+              color: {{ lesson_period.lesson.subject.colour_fg }};
             {% endif %}
-
-            {# Class or room > Display teacher #}
-            {% if type == "room" or type == "group" %}
-              {% include "chronos/partials/teachers.html" with teachers=lesson_period.lesson.teachers.all %}
+            {% if lesson_period.lesson.subject.colour_bg %}
+              background-color: {{ lesson_period.lesson.subject.colour_bg }};
             {% endif %}
+          {% endif %}
+        {% endif %}
+        "
+        {# Add CSS class for sub when it's a sub #}
+     class="{% if lesson_period.get_substitution and smart %}lesson-with-sub{% endif %}"
+>
+  <p>
+    {% if lesson_period.is_hol and smart %}
+      {# Do nothing #}
+    {% elif lesson_period.get_substitution and smart %}
+      {% with sub=lesson_period.get_substitution %}
+        {# SUBSTITUTION #}
+        {% if type == "room" and lesson_period.room != lesson_period.get_room and lesson_period.get_room != el %}
+          {# When it's the old room, let it empty #}
+
+        {% elif sub.cancelled or sub.cancelled_for_teachers %}
+          {# When a badge (cancellation, etc.) exists, then display it with the teacher#}
+
+          {# Class or room > Display teacher #}
+          {% if type == "group" or type == "room" and lesson_period.lesson.teachers.all %}
+            {% include "chronos/partials/teachers.html" with teachers=lesson_period.lesson.teachers.all %}<br/>
+          {% endif %}
+
+          {# Badge #}
+          {% include "chronos/partials/subs/badge.html" with sub=sub %}
+
+        {% else %}
+          {# Display sub #}
+
+          {# Teacher or room > display classes #}
+          {% if type == "teacher" or type == "room" %}
+            {% include "chronos/partials/groups.html" with groups=lesson_period.lesson.groups.all %}
+          {% endif %}
+
+          {# Display teacher with tooltip #}
+          {% include "chronos/partials/subs/teachers.html" with type="substitution" el=sub %}
+
+          {# Display subject #}
+          {% include "chronos/partials/subs/subject.html" with type="substitution" el=sub %}
+
+          {# Teacher or class > display room #}
+          {% if type == "teacher" or type == "group" %}
+            {% include "chronos/partials/subs/room.html" with type="substitution" el=sub %}
+          {% endif %}
+        {% endif %}
+
+
+        {# When it isn't a room or the old plan, then display the extra text (e. g. work orders) #}
+        {% if not lesson_period.room == lesson_period.get_room and lesson_period.get_room != el and sub.comment %}
+          <br>
+          <small>
+            <em>{{ lesson_period.get_substitution.comment }}</em>
+          </small>
+        {% endif %}
+      {% endwith %}
+
+
+    {% else %}
+      {# Normal plan #}
 
-            {# Display subject #}
-            <strong>
+      {# Teacher or room > Display classes #}
+      {% if type == "teacher" or type == "room" %}
+        {#                            {{ element_container.element.classes }}#}
+        {% if lesson_period.lesson.groups %}
+          {% include "chronos/partials/groups.html" with groups=lesson_period.lesson.groups.all %}
+        {% endif %}
+      {% endif %}
+
+      {# Class or room > Display teacher #}
+      {% if type == "room" or type == "group" %}
+        {% include "chronos/partials/teachers.html" with teachers=lesson_period.lesson.teachers.all %}
+      {% endif %}
+
+      {# Display subject #}
+      <strong>
               <span data-position="bottom" class="tooltipped"
                     data-tooltip="{{ lesson_period.lesson.subject.name }}">{{ lesson_period.lesson.subject.abbrev }}</span>
-            </strong>
+      </strong>
 
-            {# Teacher or class > Display room #}
-            {% if type == "teacher" or type == "group" %}
-              {% if lesson_period.room %}
-                <span class="tooltipped" data-position="bottom"
-                      data-tooltip="{{ lesson_period.room.name }}">
+      {# Teacher or class > Display room #}
+      {% if type == "teacher" or type == "group" %}
+        {% if lesson_period.room %}
+          <span class="tooltipped" data-position="bottom"
+                data-tooltip="{{ lesson_period.room.name }}">
                                   <a href="{% url "timetable" "room" lesson_period.room.pk %}">
                                         {{ lesson_period.room.short_name }}
                                     </a>
                             </span>
-              {% endif %}
-            {% endif %}
-          {% endif %}
-        </p>
-      </div>
-
-    {% endfor %}
-  </div>
+        {% endif %}
+      {% endif %}
+    {% endif %}
+  </p>
 </div>
diff --git a/aleksis/apps/chronos/templates/chronos/partials/lessons_col.html b/aleksis/apps/chronos/templates/chronos/partials/lessons_col.html
index abb2710a6a147d2bbc67ab17be784a6c52f5da69..25ddedc47f422916b2e44f5419151e1a36ce8332 100644
--- a/aleksis/apps/chronos/templates/chronos/partials/lessons_col.html
+++ b/aleksis/apps/chronos/templates/chronos/partials/lessons_col.html
@@ -1,10 +1,30 @@
-{% for period, lessons in lesson_periods.items %}
+{% if holiday %}
   <div class="row">
-    <div class="col s4">
-      {% include "chronos/partials/period_time.html" with period=period periods=periods %}
-    </div>
-    <div class="col s8">
-      {% include "chronos/partials/lesson.html" with lessons=lessons %}
+    <div class="col s12">
+      <div class="card col s12 holiday-card">
+        <div class="card-content">
+          <p>
+            {% include "chronos/partials/holiday.html" with holiday=holiday %}<br/>
+          </p>
+        </div>
+      </div>
     </div>
   </div>
-{% endfor %}
+{% else %}
+  {% for row in timetable %}
+    <div class="row">
+      <div class="col s4">
+        {% if row.type == "period" %}
+          {% include "chronos/partials/period_time.html" with period=row.period periods=periods %}
+        {% endif %}
+      </div>
+      <div class="col s8">
+        {% if row.type == "period" %}
+          {% include "chronos/partials/elements.html" with elements=row.col %}
+        {% else %}
+          {% include "chronos/partials/supervision.html" with supervision=row.col %}
+        {% endif %}
+      </div>
+    </div>
+  {% endfor %}
+{% endif %}
diff --git a/aleksis/apps/chronos/templates/chronos/partials/subs/badge.html b/aleksis/apps/chronos/templates/chronos/partials/subs/badge.html
new file mode 100644
index 0000000000000000000000000000000000000000..c04c9dd331eeb4f271ae5e7c212b43a776c71a51
--- /dev/null
+++ b/aleksis/apps/chronos/templates/chronos/partials/subs/badge.html
@@ -0,0 +1,7 @@
+{% load i18n %}
+
+{% if sub.cancelled %}
+  <span class="badge new green">{% trans "Cancelled" %}</span>
+{% elif item.el.cancelled_for_teachers %}
+  <span class="badge new green">{% trans "Cancelled for teachers" %}</span>
+{% endif %}
diff --git a/aleksis/apps/chronos/templates/chronos/partials/subs/colour.html b/aleksis/apps/chronos/templates/chronos/partials/subs/colour.html
new file mode 100644
index 0000000000000000000000000000000000000000..61979467194ad62262a9c639d414db0dadefe260
--- /dev/null
+++ b/aleksis/apps/chronos/templates/chronos/partials/subs/colour.html
@@ -0,0 +1,9 @@
+{% if item.type == "substitution" %}
+  {% if item.el.cancelled or item.el.cancelled_for_teachers %}
+    green-text
+  {% else %}
+    black-text
+  {% endif %}
+{% elif item.type == "supervision_substitution" %}
+  blue-text
+{% endif %}
diff --git a/aleksis/apps/chronos/templates/chronos/partials/subs/period.html b/aleksis/apps/chronos/templates/chronos/partials/subs/period.html
new file mode 100644
index 0000000000000000000000000000000000000000..8e8d31bd2c48e14cc28330cc948e3d43882d6f2f
--- /dev/null
+++ b/aleksis/apps/chronos/templates/chronos/partials/subs/period.html
@@ -0,0 +1,9 @@
+<strong>
+  {% if type == "substitution" %}
+    {{ el.lesson_period.period.period }}.
+  {% elif type == "supervision_substitution" %}
+    {% with break=el.supervision.break_item %}
+      {{ break.after_period_number }}./{{ break.before_period_number }}.
+    {% endwith %}
+  {% endif %}
+</strong>
diff --git a/aleksis/apps/chronos/templates/chronos/partials/subs/room.html b/aleksis/apps/chronos/templates/chronos/partials/subs/room.html
index cf637b92303dfe31d1c19dc781809f1c3c10dac3..49010706ce2a67d2ca37e3e0752261398090525d 100644
--- a/aleksis/apps/chronos/templates/chronos/partials/subs/room.html
+++ b/aleksis/apps/chronos/templates/chronos/partials/subs/room.html
@@ -1,47 +1,45 @@
-{% if not sub.is_event %}
-  {% if sub.sub.type == 3 %}
-    {# Supervisement #}
-    {{ sub.sub.corridor.name }}
-  {% elif sub.sub.type == 1 or sub.sub.type == 2 %}
+{% if type == "substitution" %}
+  {% if el.cancelled or el.cancelled_for_teachers %}
     {# Canceled lesson: no room #}
-  {% elif sub.room and sub.lesson_period.room %}
+  {% elif el.room and el.lesson_period.room %}
     {# New and old room available #}
     <span class="tooltipped" data-position="bottom"
-          data-tooltip="{{ sub.lesson_period.room.name }} → {{ sub.lesson_period.room.name }}">
-            <a href="{% url "timetable" "room" sub.lesson_period.room.pk %}">
-                <s>{{ sub.lesson_period.room.short_name }}</s>
-            </a>
-            →
-            <a href="{% url "timetable" "room" sub.room.pk %}">
-                <strong>{{ sub.room.short_name }}</strong>
-            </a>
-        </span>
-  {% elif sub.room and not sub.lesson_period.room %}
+          data-tooltip="{{ el.lesson_period.room.name }} → {{ el.lesson_period.room.name }}"
+          title="{{ el.lesson_period.room.name }} → {{ el.lesson_period.room.name }}">
+      <a href="{% url "timetable" "room" el.lesson_period.room.pk %}">
+          <s>{{ el.lesson_period.room.short_name }}</s>
+      </a>
+      →
+      <a href="{% url "timetable" "room" el.room.pk %}">
+          <strong>{{ el.room.short_name }}</strong>
+      </a>
+    </span>
+  {% elif el.room and not el.lesson_period.room %}
     {# Only new room available #}
     <span class="tooltipped" data-position="bottom"
-          data-tooltip="{{ sub.room.name }}">
-            <a href="{% url "timetable" "room" sub.room.pk %}">
-                {{ sub.room.short_name }}
-            </a>
-        </span>
-  {% elif not sub.room and not sub.lesson_period.room %}
+          data-tooltip="{{ el.room.name }}"
+          title="{{ el.room.name }}">
+      <a href="{% url "timetable" "room" el.room.pk %}">
+          {{ el.room.short_name }}
+      </a>
+    </span>
+  {% elif not el.room and not el.lesson_period.room %}
     {# Nothing to view #}
   {% else %}
     {# Only old room available #}
     <span class="tooltipped" data-position="bottom"
-          data-tooltip="{{ sub.lesson_period.room.name }}">
-            <a href="{% url "timetable" "room" sub.lesson_period.room.pk %}">
-                {{ sub.lesson_period.room.short_name }}
-            </a>
-        </span>
+          data-tooltip="{{ el.lesson_period.room.name }}"
+          title="{{ el.lesson_period.room.name }}">
+      <a href="{% url "timetable" "room" el.lesson_period.room.pk %}">
+          {{ el.lesson_period.room.short_name }}
+      </a>
+    </span>
   {% endif %}
-{% else %}
-  {% for room in sub.rooms %}
-    <span class="tooltipped" data-position="bottom"
-          data-tooltip="{{ room.name }}">
-            <a href="{% url "timetable_smart_plan" "room" room.id %}">
-                <strong>{{ room.short_name }}{% if not forloop.last %},{% endif %}</strong>
-            </a>
-        </span>
-  {% endfor %}
+{% elif type == "supervision_substitution" %}
+  {% with supervision=el.supervision %}
+    <span data-position="bottom" class="tooltipped"
+          data-tooltip="{{ supervision.area.name }}" title="{{ supervision.area.name }}">
+      {{ supervision.area.short_name }}
+    </span>
+  {% endwith %}
 {% endif %}
diff --git a/aleksis/apps/chronos/templates/chronos/partials/subs/subject.html b/aleksis/apps/chronos/templates/chronos/partials/subs/subject.html
index 6fe8eb5a02bb7a6afb75adf50012efa9d2712239..e6c9e86d016237276a2d5c894addba10c8a5942f 100644
--- a/aleksis/apps/chronos/templates/chronos/partials/subs/subject.html
+++ b/aleksis/apps/chronos/templates/chronos/partials/subs/subject.html
@@ -1,28 +1,28 @@
 {% load i18n %}
 
-{% if not sub.sub.is_event %}
-  {% if sub.sub.type == 3 %}
-    <strong>{% trans "Supervision" %}</strong>
-  {% elif not sub.lesson_period.lesson.subject and not sub.subject %}
-  {% elif sub.sub.type == 1 or sub.sub.type == 2 %}
-    <span data-position="bottom" class="tooltipped" data-tooltip="{{ sub.lesson_period.lesson.subject.name }}">
-      <s>{{ sub.lesson_period.lesson.subject.abbrev }}</s>
-    </span>
-  {% elif sub.subject and sub.lesson_period.lesson.subject %}
-    <span data-position="bottom" class="tooltipped" data-tooltip="{{ sub.lesson_period.lesson.subject.name }}">
-      <s>{{ sub.lesson_period.lesson.subject.abbrev }}</s>
-    </span>
+{% if type == "substitution" %}
+  {% if not el.lesson_period.lesson.subject and not el.subject %}
+  {% elif el.cancelled or el.cancelled_for_teachers %}
+    <span data-position="bottom" class="tooltipped" data-tooltip="{{ el.lesson_period.lesson.subject.name }}">
+    <s>{{ el.lesson_period.lesson.subject.abbrev }}</s>
+  </span>
+  {% elif el.subject and el.lesson_period.lesson.subject %}
+    <span data-position="bottom" class="tooltipped" data-tooltip="{{ el.lesson_period.lesson.subject.name }}">
+    <s>{{ el.lesson_period.lesson.subject.abbrev }}</s>
+  </span>
     →
-    <span data-position="bottom" class="tooltipped" data-tooltip="{{ sub.subject.name }}">
-      <strong>{{ sub.subject.abbrev }}</strong>
-    </span>
-  {% elif sub.subject and not sub.lesson_period.lesson.subject %}
-    <span data-position="bottom" class="tooltipped" data-tooltip="{{ sub.subject.name }}">
-      <strong>{{ sub.subject.abbrev }}</strong>
-    </span>
+    <span data-position="bottom" class="tooltipped" data-tooltip="{{ el.subject.name }}">
+    <strong>{{ el.subject.abbrev }}</strong>
+  </span>
+  {% elif el.subject and not el.lesson_period.lesson.subject %}
+    <span data-position="bottom" class="tooltipped" data-tooltip="{{ el.subject.name }}">
+    <strong>{{ el.subject.abbrev }}</strong>
+  </span>
   {% else %}
-    <span data-position="bottom" class="tooltipped" data-tooltip="{{ sub.lesson_period.lesson.subject.name }}">
-      <strong>{{ sub.lesson_period.lesson.subject.abbrev }}</strong>
-    </span>
+    <span data-position="bottom" class="tooltipped" data-tooltip="{{ el.lesson_period.lesson.subject.name }}">
+    <strong>{{ el.lesson_period.lesson.subject.abbrev }}</strong>
+  </span>
   {% endif %}
+{% elif type == "supervision_substitution" %}
+  {% trans "Supervision" %}
 {% endif %}
diff --git a/aleksis/apps/chronos/templates/chronos/partials/subs/teachers.html b/aleksis/apps/chronos/templates/chronos/partials/subs/teachers.html
index ec17d8b811cfad34c1cad90d2a3302aa5752ba53..4fbd63fea66282e17f77870ea3d16ec47fb31da4 100644
--- a/aleksis/apps/chronos/templates/chronos/partials/subs/teachers.html
+++ b/aleksis/apps/chronos/templates/chronos/partials/subs/teachers.html
@@ -1,19 +1,25 @@
-{% if not sub.is_event %}
-  {% if sub.sub.type == 1 and sub.lesson_period.lesson.teachers.all %}
-    {% include "chronos/partials/teachers.html" with teachers=sub.lesson_period.lesson.teachers.all %}
-  {% elif sub.teachers.all and sub.lesson_period.lesson.teachers.all %}
+{% if type == "substitution" %}
+  {% if el.cancelled and el.lesson_period.lesson.teachers.all %}
+    {% include "chronos/partials/teachers.html" with teachers=el.lesson_period.lesson.teachers.all %}
+  {% elif el.teachers.all and el.lesson_period.lesson.teachers.all %}
     <s>
-      {% include "chronos/partials/teachers.html" with teachers=sub.lesson_period.lesson.teachers.all %}
+      {% include "chronos/partials/teachers.html" with teachers=el.lesson_period.lesson.teachers.all %}
     </s>
     →
     <strong>
-      {% include "chronos/partials/teachers.html" with teachers=sub.teachers.all %}
+      {% include "chronos/partials/teachers.html" with teachers=el.teachers.all %}
     </strong>
-  {% elif sub.teachers.all and not sub.lesson_period.lesson.teachers.all %}
-    {% include "chronos/partials/teachers.html" with teachers=sub.teachers.all %}
-  {% elif sub.lesson_period.lesson.teachers.all %}
-    {% include "chronos/partials/teachers.html" with teachers=sub.lesson_period.lesson.teachers.all %}
+  {% elif el.teachers.all and not el.lesson_period.lesson.teachers.all %}
+    {% include "chronos/partials/teachers.html" with teachers=el.teachers.all %}
+  {% elif el.lesson_period.lesson.teachers.all %}
+    {% include "chronos/partials/teachers.html" with teachers=el.lesson_period.lesson.teachers.all %}
   {% endif %}
-{% else %}
-  {% include "chronos/partials/teachers.html" with teachers=sub.teachers.all %}
+{% elif type == "supervision_substitution" %}
+  <s>
+    {% include "chronos/partials/teachers.html" with teachers=el.supervision.teachers %}
+  </s>
+  →
+  <strong>
+    {% include "chronos/partials/teachers.html" with teachers=el.teachers %}
+  </strong>
 {% endif %}
diff --git a/aleksis/apps/chronos/templates/chronos/partials/supervision.html b/aleksis/apps/chronos/templates/chronos/partials/supervision.html
new file mode 100644
index 0000000000000000000000000000000000000000..10ff51888939929b2bb3183c254b5a7838b91a12
--- /dev/null
+++ b/aleksis/apps/chronos/templates/chronos/partials/supervision.html
@@ -0,0 +1,29 @@
+{% load i18n %}
+
+<div class="card lesson-card supervision-card">
+  <div class="card-content">
+    {% if supervision %}
+      <div style="
+              {% if supervision.area.colour_fg %}
+                color: {{ supervision.area.colour_fg }};
+              {% endif %}
+              {% if supervision.area.colour_bg %}
+                background-color: {{ supervision.area.colour_bg }};
+              {% endif %}
+              " class="{% if supervision.get_substitution and smart %}lesson-with-sub{% endif %}">
+        <p>
+          <strong>{% trans "Supervision" %}</strong>
+          <span data-position="bottom" class="tooltipped"
+                data-tooltip="{{ supervision.area.name }}" title="{{ supervision.area.name }}">
+            {{ supervision.area.short_name }}
+          </span>
+          {% if supervision.get_substitution and smart %}
+            {% include "chronos/partials/subs/teachers.html" with type="supervision_substitution" el=supervision.get_substitution %}
+          {% elif type == "supervision_area" %}
+            {% include "chronos/partials/teachers.html" with teachers=supervision.teachers %}
+          {% endif %}
+        </p>
+      </div>
+    {% endif %}
+  </div>
+</div>
diff --git a/aleksis/apps/chronos/templates/chronos/partials/week_select.html b/aleksis/apps/chronos/templates/chronos/partials/week_select.html
new file mode 100644
index 0000000000000000000000000000000000000000..6739290f11b07a2b787d3789f912bc284d6f2ec2
--- /dev/null
+++ b/aleksis/apps/chronos/templates/chronos/partials/week_select.html
@@ -0,0 +1,45 @@
+{% load i18n %}
+
+<div class="col s12 m6 right">
+  <div class="col s2 no-print">
+    <a class="waves-effect waves-teal btn-flat btn-flat-medium right" href="{{ url_prev }}">
+      <i class="material-icons center">navigate_before</i>
+    </a>
+  </div>
+
+  <div class="input-field col s8 no-margin hide-on-med-and-up">
+    <select id="calendar-week-1">
+      {% for week in weeks %}
+        <option value="{{ week.week }}" {% if week == wanted_week %}
+                selected {% endif %}>{% trans "CW" %} {{ week.week }} ({{ week.0|date:"SHORT_DATE_FORMAT" }}–{{ week.6|date:"SHORT_DATE_FORMAT" }})
+        </option>
+      {% endfor %}
+    </select>
+  </div>
+
+  <div class="input-field col s8 no-margin hide-on-med-and-down">
+    <select id="calendar-week-2">
+      {% for week in weeks %}
+        <option value="{{ week.week }}" {% if week == wanted_week %}
+                selected {% endif %}>{% trans "CW" %} {{ week.week }} ({{ week.0|date:"j.n." }}–{{ week.6|date:"SHORT_DATE_FORMAT" }})
+        </option>
+      {% endfor %}
+    </select>
+  </div>
+
+  <div class="input-field col s8 no-margin hide-on-small-and-down hide-on-large-only">
+    <select id="calendar-week-3">
+      {% for week in weeks %}
+        <option value="{{ week.week }}" {% if week == wanted_week %}
+                selected {% endif %}>{% trans "CW" %} {{ week.week }} ({{ week.0|date:"j.n." }}–{{ week.6|date:"j.n." }})
+        </option>
+      {% endfor %}
+    </select>
+  </div>
+
+  <div class="col s2 no-print">
+    <a class="waves-effect waves-teal btn-flat btn-flat-medium left" href="{{ url_next }}">
+      <i class="material-icons center">navigate_next</i>
+    </a>
+  </div>
+</div>
diff --git a/aleksis/apps/chronos/templates/chronos/substitutions.html b/aleksis/apps/chronos/templates/chronos/substitutions.html
index febc6aeb9313a6ac73e75bd2a720093f530ef2a4..eff5da58288939ad8906126ae80666b903f75074 100644
--- a/aleksis/apps/chronos/templates/chronos/substitutions.html
+++ b/aleksis/apps/chronos/templates/chronos/substitutions.html
@@ -57,37 +57,34 @@
         </p>
       </td>
     {% endif %}
-    {% for sub in substitutions %}
-      <tr class="{% if sub.type_ == "cancellation" %}green-text{% else %}black-text{% endif %}">
-        {# TODO: Extend support for blue and purple (supervisions and events) #}
+    {% for item in substitutions %}
+      <tr class="{% include "chronos/partials/subs/colour.html" with item=item %}">
+        {# TODO: Extend support for purple (events) #}
         <td>
-          {% include "chronos/partials/groups.html" with groups=sub.lesson_period.lesson.groups.all %}
+          {% if item.type == "substitution" %}
+            {% include "chronos/partials/groups.html" with groups=item.el.lesson_period.lesson.groups.all %}
+          {% endif %}
         </td>
         <td>
-          <strong>
-            {{ sub.lesson_period.period.period }}.
-          </strong>
+         {% include "chronos/partials/subs/period.html" with type=item.type el=item.el %}
         </td>
         <td>
-          {% include "chronos/partials/subs/teachers.html" %}
+          {% include "chronos/partials/subs/teachers.html" with type=item.type el=item.el %}
         </td>
         <td>
-          {% include "chronos/partials/subs/subject.html" %}
+          {% include "chronos/partials/subs/subject.html" with type=item.type el=item.el %}
         </td>
         <td>
-          {% include "chronos/partials/subs/room.html" %}
+          {% include "chronos/partials/subs/room.html" with type=item.type el=item.el %}
         </td>
         <td>
-          {% if sub.cancelled %}
-            {# TODO: Support other cases#}
-            <span class="badge new green hide-on-med-and-up">{% trans "Cancelled" %}</span>
-          {% endif %}
-          {#          <em>{{ sub.text|default:"" }}</em>#}
+          <span class="hide-on-med-and-up">
+            {% include "chronos/partials/subs/badge.html" with sub=item.el %}
+          </span>
+          <em>{{ sub.comment|default:"" }}</em>
         </td>
         <td class="hide-on-small-and-down">
-          {% if sub.cancelled %}
-            <span class="badge new green darken-2">{% trans "Cancelled" %}</span>
-          {% endif %}
+          {% include "chronos/partials/subs/badge.html" with sub=item.el %}
         </td>
       </tr>
     {% endfor %}
diff --git a/aleksis/apps/chronos/templates/chronos/substitutions_print.html b/aleksis/apps/chronos/templates/chronos/substitutions_print.html
index 400124f2391306d6353af0c2a5819d97ad89a228..14ecd637f2ba24a2916921ab955e2942b366780b 100644
--- a/aleksis/apps/chronos/templates/chronos/substitutions_print.html
+++ b/aleksis/apps/chronos/templates/chronos/substitutions_print.html
@@ -46,38 +46,35 @@
       {% endif %}
 
       <tbody>
-      {% for sub in c.substitutions %}
-        <tr class="{% if sub.type_ == "cancellation" %}green-text{% else %}black-text{% endif %} ">
+      {% for item in c.substitutions %}
+        {% ifchanged item.el.lesson_period.lesson.groups_to_show_names %}
+          </tbody>
+          <tbody class="{% cycle "striped" "not-striped" %}">
+        {% endifchanged %}
+
+        <tr class="{% include "chronos/partials/subs/colour.html" with item=item %}">
           <td>
-            {% include "chronos/partials/groups.html" with groups=sub.lesson_period.lesson.groups.all %}
+            {% if item.type == "substitution" %}
+              {% include "chronos/partials/groups.html" with groups=item.el.lesson_period.lesson.groups.all %}
+            {% endif %}
           </td>
           <td>
-            <strong>
-              {{ sub.lesson_period.period.period }}.
-            </strong>
+            {% include "chronos/partials/subs/period.html" with type=item.type el=item.el %}
           </td>
           <td>
-            {% include "chronos/partials/subs/teachers.html" %}
+            {% include "chronos/partials/subs/teachers.html" with type=item.type el=item.el %}
           </td>
           <td>
-            {% include "chronos/partials/subs/subject.html" %}
+            {% include "chronos/partials/subs/subject.html" with type=item.type el=item.el %}
           </td>
           <td>
-            {% include "chronos/partials/subs/room.html" %}
+            {% include "chronos/partials/subs/room.html" with type=item.type el=item.el %}
           </td>
           <td>
-            {% if sub.cancelled %}
-              {# TODO: Support other cases#}
-              <span class="badge new green">{% trans "Cancelled" %}</span>
-            {% endif %}
-            {#          <em>{{ sub.text|default:"" }}</em>#}
+            {% include "chronos/partials/subs/badge.html" with sub=item.el %}
+            <em>{{ sub.comment|default:"" }}</em>
           </td>
         </tr>
-
-        {% ifchanged sub.lesson_period.lesson.groups %}
-          </tbody>
-          <tbody class="{% cycle "not-striped" "striped" %}">
-        {% endifchanged %}
       {% endfor %}
       </tbody>
     </table>
diff --git a/aleksis/apps/chronos/templates/chronos/timetable.html b/aleksis/apps/chronos/templates/chronos/timetable.html
index 397eca05446fb5d609144a460a0c6270fe660f40..88b02be677b238d9b05d71e52104f19a12de47bb 100644
--- a/aleksis/apps/chronos/templates/chronos/timetable.html
+++ b/aleksis/apps/chronos/templates/chronos/timetable.html
@@ -57,51 +57,7 @@
       </div>
 
       {# Week select #}
-      <div class="col s12 m6 right">
-        <div class="col s2 no-print">
-          <a class="waves-effect waves-teal btn-flat btn-flat-medium right" href="{{ url_prev }}">
-            <i class="material-icons center">navigate_before</i>
-          </a>
-        </div>
-
-        {% with wanted_week=week %}
-          <div class="input-field col s8 no-margin hide-on-med-and-up">
-            <select id="calendar-week-1">
-              {% for week in weeks %}
-                <option value="{{ week.week }}" {% if week == wanted_week %}
-                        selected {% endif %}>{% trans "CW" %} {{ week.week }} ({{ week.0|date:"SHORT_DATE_FORMAT" }}–{{ week.6|date:"SHORT_DATE_FORMAT" }})
-                </option>
-              {% endfor %}
-            </select>
-          </div>
-
-          <div class="input-field col s8 no-margin hide-on-med-and-down">
-            <select id="calendar-week-2">
-              {% for week in weeks %}
-                <option value="{{ week.week }}" {% if week == wanted_week %}
-                        selected {% endif %}>{% trans "CW" %} {{ week.week }} ({{ week.0|date:"j.n" }}–{{ week.6|date:"SHORT_DATE_FORMAT" }})
-                </option>
-              {% endfor %}
-            </select>
-          </div>
-
-          <div class="input-field col s8 no-margin hide-on-small-and-down hide-on-large-only">
-            <select id="calendar-week-3">
-              {% for week in weeks %}
-                <option value="{{ week.week }}" {% if week == wanted_week %}
-                        selected {% endif %}>{% trans "CW" %} {{ week.week }} ({{ week.0|date:"j.n" }}–{{ week.6|date:"j.n" }})
-                </option>
-              {% endfor %}
-            </select>
-          </div>
-        {% endwith %}
-
-        <div class="col s2 no-print">
-          <a class="waves-effect waves-teal btn-flat btn-flat-medium left" href="{{ url_next }}">
-            <i class="material-icons center">navigate_next</i>
-          </a>
-        </div>
-      </div>
+      {% include "chronos/partials/week_select.html" with wanted_week=week %}
 
     {% else %}
       {# Show if regular #}
@@ -124,34 +80,34 @@
 
       </div>
       {# Show short weekdays on tablets #}
-      {% for i, weekday, date in weekdays_short %}
+      {% for weekday in weekdays_short %}
         <div class="col s2 hide-on-large-only">
           <div class="card timetable-title-card">
             <div class="card-content">
-                            <span class="card-title">
-                                {{ weekday }}
-                            </span>
-              {{ date }}
-              {#              {% if day.1 %}#}
-              {#                <span class="badge new blue center-align holiday-badge">{{ day.1.0 }}</span>#}
-              {#              {% endif %}#}
+              <span class="card-title">
+                {{ weekday.name }}
+              </span>
+              {{ weekday.date }}
+              {% if weekday.holiday %}
+                <br/>{% include "chronos/partials/holiday.html" with holiday=weekday.holiday %}
+              {% endif %}
             </div>
           </div>
         </div>
       {% endfor %}
 
       {# Show long weekdays elsewere #}
-      {% for i, weekday, date in weekdays %}
+      {% for weekday in weekdays %}
         <div class="col s2 hide-on-med-only">
           <div class="card timetable-title-card">
             <div class="card-content">
-                            <span class="card-title">
-                                {{ weekday }}
-                            </span>
-              {{ date }}
-              {#              {% if day.1 %}#}
-              {#                <span class="badge new blue center-align holiday-badge">{{ day.1.0 }}</span>#}
-              {#              {% endif %}#}
+              <span class="card-title">
+                {{ weekday.name }}
+              </span>
+              {{ weekday.date }}
+              {% if weekday.holiday %}
+                <br/>{% include "chronos/partials/holiday.html" with holiday=weekday.holiday %}
+              {% endif %}
             </div>
           </div>
         </div>
@@ -159,17 +115,22 @@
     </div>
 
     {#  Lessons #}
-    {% for period, lesson_periods_period in lesson_periods.items %}
-
+    {% for row in timetable %}
       <div class="row">
         <div class="col s2">
-          {% include "chronos/partials/period_time.html" with period=period periods=periods %}
+          {% if row.type == "period" %}
+            {% include "chronos/partials/period_time.html" with period=row.period periods=periods %}
+          {% endif %}
         </div>
 
-        {% for weekday, lessons in lesson_periods_period.items %}
+        {% for col in row.cols %}
           {# A lesson #}
           <div class="col s2">
-            {% include "chronos/partials/lesson.html" with lessons=lessons %}
+            {% if row.type == "period" %}
+              {% include "chronos/partials/elements.html" with elements=col %}
+            {% else %}
+              {% include "chronos/partials/supervision.html" with supervision=col %}
+            {% endif %}
           </div>
         {% endfor %}
       </div>
@@ -178,30 +139,33 @@
 
   {# show 5 seperate ones on mobiles #}
   <div class="timetable-plan hide-on-med-and-up">
-    {% for i, weekday, date in weekdays %}
+    {% for weekday in weekdays %}
       <div class="card timetable-mobile-title-card">
         <div class="card-content">
-                    <span class="card-title">
-                        {{ weekday }}
-                    </span>
-          {{ date }}
-          {#          {% if day.1 %}#}
-          {#            <span class="badge new blue center-align holiday-badge">{{ day.1.0 }}</span>#}
-          {#          {% endif %}#}
-          {#          &nbsp;#}
+          <span class="card-title">
+            {{ weekday.name }}
+          </span>
+          {{ weekday.date }}
+          {% if weekday.holiday %}
+            <br/>{% include "chronos/partials/holiday.html" with holiday=weekday.holiday %}
+          {% endif %}
         </div>
       </div>
-      {% for period, lesson_periods_period in lesson_periods.items %}
+      {% for row in timetable %}
         <div class="row">
           <div class="col s4">
-            {% include "chronos/partials/period_time.html" with period=period periods=periods %}
+            {% include "chronos/partials/period_time.html" with period=row.period periods=periods %}
           </div>
 
-          {% for weekday, lessons in lesson_periods_period.items %}
-            {% if forloop.counter0 == i %}
+          {% for col in row.cols %}
+            {% if forloop.counter0 == weekday.key %}
               <div class="col s8">
                 {# A lesson #}
-                {% include "chronos/partials/lesson.html" with lessons=lessons %}
+                {% if row.type == "period" %}
+                  {% include "chronos/partials/elements.html" with elements=col %}
+                {% else %}
+                  {% include "chronos/partials/supervision.html" with supervision=col %}
+                {% endif %}
               </div>
             {% endif %}
           {% endfor %}
diff --git a/aleksis/apps/chronos/util/build.py b/aleksis/apps/chronos/util/build.py
new file mode 100644
index 0000000000000000000000000000000000000000..7148ce40928eb253a6292c28f470de1dcb9319ac
--- /dev/null
+++ b/aleksis/apps/chronos/util/build.py
@@ -0,0 +1,305 @@
+from collections import OrderedDict
+from datetime import date
+from typing import Union, List, Tuple
+
+from calendarweek import CalendarWeek
+from django.apps import apps
+
+from aleksis.core.models import Person
+
+LessonPeriod = apps.get_model("chronos", "LessonPeriod")
+TimePeriod = apps.get_model("chronos", "TimePeriod")
+Break = apps.get_model("chronos", "Break")
+Supervision = apps.get_model("chronos", "Supervision")
+LessonSubstitution = apps.get_model("chronos", "LessonSubstitution")
+SupervisionSubstitution = apps.get_model("chronos", "SupervisionSubstitution")
+Event = apps.get_model("chronos", "Event")
+Holiday = apps.get_model("chronos", "Holiday")
+
+
+def build_timetable(
+    type_: str, obj: Union[int, Person], date_ref: Union[CalendarWeek, date]
+):
+    needed_breaks = []
+
+    if not isinstance(obj, int):
+        pk = obj.pk
+    else:
+        pk = obj
+
+    is_person = False
+    if type_ == "person":
+        is_person = True
+        type_ = obj.timetable_type
+
+    # Get matching holidays
+    if is_person:
+        holiday = Holiday.on_day(date_ref)
+    else:
+        holidays_per_weekday = Holiday.in_week(date_ref)
+
+    # Get matching lesson periods
+    if is_person:
+        lesson_periods = LessonPeriod.objects.daily_lessons_for_person(obj, date_ref)
+    else:
+        lesson_periods = LessonPeriod.objects.in_week(date_ref).filter_from_type(
+            type_, obj
+        )
+
+    # Sort lesson periods in a dict
+    lesson_periods_per_period = {}
+    for lesson_period in lesson_periods:
+        period = lesson_period.period.period
+        weekday = lesson_period.period.weekday
+
+        if period not in lesson_periods_per_period:
+            lesson_periods_per_period[period] = [] if is_person else {}
+
+        if not is_person and weekday not in lesson_periods_per_period[period]:
+            lesson_periods_per_period[period][weekday] = []
+
+        if is_person:
+            lesson_periods_per_period[period].append(lesson_period)
+        else:
+            lesson_periods_per_period[period][weekday].append(lesson_period)
+
+    # Get events
+    if is_person:
+        events = Event.objects.on_day(date_ref).filter_from_person(obj)
+    else:
+        events = Event.objects.in_week(date_ref).filter_from_type(type_, obj)
+
+    # Sort events in a dict
+    events_per_period = {}
+    for event in events:
+        if not is_person and event.date_start < date_ref[TimePeriod.weekday_min]:
+            # If start date not in current week, set weekday and period to min
+            weekday_from = TimePeriod.weekday_min
+            period_from_first_weekday = TimePeriod.period_min
+        else:
+            weekday_from = event.date_start.weekday()
+            period_from_first_weekday = event.period_from.period
+
+        if not is_person and event.date_end > date_ref[TimePeriod.weekday_max]:
+            # If end date not in current week, set weekday and period to max
+            weekday_to = TimePeriod.weekday_max
+            period_to_last_weekday = TimePeriod.period_max
+        else:
+            weekday_to = event.date_end.weekday()
+            period_to_last_weekday = event.period_to.period
+
+        for weekday in range(weekday_from, weekday_to + 1):
+            if is_person and weekday != date_ref.weekday():
+                # If daily timetable for person, skip other weekdays
+                continue
+
+            if weekday == weekday_from:
+                # If start day, use start period
+                period_from = period_from_first_weekday
+            else:
+                # If not start day, use min period
+                period_from = TimePeriod.period_min
+
+            if weekday == weekday_to:
+                # If end day, use end period
+                period_to = period_to_last_weekday
+            else:
+                # If not end day, use max period
+                period_to = TimePeriod.period_max
+
+            for period in range(period_from, period_to +1):
+                if period not in events_per_period:
+                    events_per_period[period] = [] if is_person else {}
+
+                if not is_person and weekday not in events_per_period[period]:
+                    events_per_period[period][weekday] = []
+
+                if is_person:
+                    events_per_period[period].append(event)
+                else:
+                    events_per_period[period][weekday].append(event)
+
+    if type_ == "teacher":
+        # Get matching supervisions
+        if is_person:
+            week = CalendarWeek.from_date(date_ref)
+        else:
+            week = date_ref
+        supervisions = Supervision.objects.all().annotate_week(week).filter_by_teacher(obj)
+
+        if is_person:
+            supervisions.filter_by_weekday(date_ref.weekday())
+
+        supervisions_per_period_after = {}
+        for supervision in supervisions:
+            weekday = supervision.break_item.weekday
+            period_after_break = supervision.break_item.before_period_number
+            print(supervision, weekday, period_after_break)
+
+            if period_after_break not in needed_breaks:
+                needed_breaks.append(period_after_break)
+
+            if (
+                not is_person
+                and period_after_break not in supervisions_per_period_after
+            ):
+                supervisions_per_period_after[period_after_break] = {}
+
+            if is_person:
+                supervisions_per_period_after[period_after_break] = supervision
+            else:
+                supervisions_per_period_after[period_after_break][weekday] = supervision
+
+    # Get ordered breaks
+    breaks = OrderedDict(sorted(Break.get_breaks_dict().items()))
+
+    rows = []
+    for period, break_ in breaks.items():  # period is period after break
+        # Break
+        if type_ == "teacher" and period in needed_breaks:
+            row = {
+                "type": "break",
+                "after_period": break_.after_period_number,
+                "before_period": break_.before_period_number,
+                "time_start": break_.time_start,
+                "time_end": break_.time_end,
+            }
+
+            if not is_person:
+                cols = []
+
+                for weekday in range(
+                    TimePeriod.weekday_min, TimePeriod.weekday_max + 1
+                ):
+                    col = None
+                    if (
+                        period in supervisions_per_period_after
+                        and weekday not in holidays_per_weekday
+                    ):
+                        if weekday in supervisions_per_period_after[period]:
+                            col = supervisions_per_period_after[period][weekday]
+                    cols.append(col)
+
+                row["cols"] = cols
+            else:
+                col = None
+                if period in supervisions_per_period_after and not holiday:
+                    col = supervisions_per_period_after[period]
+                row["col"] = col
+            rows.append(row)
+
+        # Period
+        if period <= TimePeriod.period_max:
+            row = {
+                "type": "period",
+                "period": period,
+                "time_start": break_.before_period.time_start,
+                "time_end": break_.before_period.time_end,
+            }
+
+            if not is_person:
+                cols = []
+                for weekday in range(
+                    TimePeriod.weekday_min, TimePeriod.weekday_max + 1
+                ):
+                    col = []
+
+                    # Add lesson periods
+                    if (
+                        period in lesson_periods_per_period
+                        and weekday not in holidays_per_weekday
+                    ):
+                        if weekday in lesson_periods_per_period[period]:
+                            col += lesson_periods_per_period[period][weekday]
+
+                    # Add events
+                    if (
+                        period in events_per_period
+                        and weekday not in holidays_per_weekday
+                    ):
+                        if weekday in events_per_period[period]:
+                            col += events_per_period[period][weekday]
+
+                    cols.append(col)
+
+                row["cols"] = cols
+            else:
+                col = []
+
+                # Add lesson periods
+                if period in lesson_periods_per_period and not holiday:
+                    col += lesson_periods_per_period[period]
+
+                # Add events
+                if period in events_per_period and not holiday:
+                    col += events_per_period[period]
+
+                row["col"] = col
+
+            rows.append(row)
+
+    return rows
+
+
+def build_substitutions_list(wanted_day: date) -> List[dict]:
+    rows = []
+
+    subs = LessonSubstitution.objects.on_day(wanted_day).order_by(
+        "lesson_period__lesson__groups", "lesson_period__period"
+    )
+
+    for sub in subs:
+        if not sub.cancelled_for_teachers:
+            sort_a = sub.lesson_period.lesson.group_names
+        else:
+            sort_a = "Z.{}".format(sub.lesson_period.lesson.teacher_names)
+
+        row = {
+            "type": "substitution",
+            "sort_a": sort_a,
+            "sort_b": "{}".format(sub.lesson_period.period.period),
+            "el": sub,
+        }
+
+        rows.append(row)
+
+    # Get supervision substitutions
+    super_subs = SupervisionSubstitution.objects.filter(date=wanted_day)
+
+    for super_sub in super_subs:
+        row = {
+            "type": "supervision_substitution",
+            "sort_a": "Z.{}".format(super_sub.teacher),
+            "sort_b": "{}".format(super_sub.supervision.break_item.after_period_number),
+            "el": super_sub
+        }
+        rows.append(row)
+
+    # Sort all items
+    def sorter(row: dict):
+        return row["sort_a"] + row["sort_b"]
+
+    rows.sort(key=sorter)
+
+    return rows
+
+
+def build_weekdays(
+    base: List[Tuple[int, str]], wanted_week: CalendarWeek
+) -> List[dict]:
+    holidays_per_weekday = Holiday.in_week(wanted_week)
+
+    weekdays = []
+    for key, name in base[TimePeriod.weekday_min : TimePeriod.weekday_max + 1]:
+
+        weekday = {
+            "key": key,
+            "name": name,
+            "date": wanted_week[key],
+            "holiday": holidays_per_weekday[key]
+            if key in holidays_per_weekday
+            else None,
+        }
+        weekdays.append(weekday)
+
+    return weekdays
diff --git a/aleksis/apps/chronos/views.py b/aleksis/apps/chronos/views.py
index 310b6f44841e28e944bb574caeb86b3feafa69d8..266910e8785a48bfff8ceea85c74109465a485fc 100644
--- a/aleksis/apps/chronos/views.py
+++ b/aleksis/apps/chronos/views.py
@@ -16,8 +16,9 @@ from aleksis.core.decorators import admin_required
 from aleksis.core.models import Person, Group, Announcement
 from aleksis.core.util import messages
 from .forms import LessonSubstitutionForm
-from .models import LessonPeriod, LessonSubstitution, TimePeriod, Room
+from .models import LessonPeriod, LessonSubstitution, TimePeriod, Room, Holiday
 from .tables import LessonsTable
+from .util.build import build_timetable, build_substitutions_list, build_weekdays
 from .util.js import date_unix
 from .util.date import CalendarWeek, get_weeks_for_year
 from aleksis.core.util.core_helpers import has_person
@@ -65,17 +66,19 @@ def my_timetable(
 
     if has_person(request.user):
         person = request.user.person
+        type_ = person.timetable_type
 
-        lesson_periods = LessonPeriod.objects.daily_lessons_for_person(person, wanted_day)
+        # Build timetable
+        timetable = build_timetable("person", person, wanted_day)
 
-        if lesson_periods is None:
+        if type_ is None:
             # If no student or teacher, redirect to all timetables
             return redirect("all_timetables")
 
-        type_ = person.timetable_type
         super_el = person.timetable_object
 
-        context["lesson_periods"] = lesson_periods.per_period_one_day()
+        context["timetable"] = timetable
+        context["holiday"] = Holiday.on_day(wanted_day)
         context["super"] = {"type": type_, "el": super_el}
         context["type"] = type_
         context["day"] = wanted_day
@@ -120,55 +123,16 @@ def timetable(
         # TODO: On not used days show next week
         wanted_week = CalendarWeek()
 
-    lesson_periods = LessonPeriod.objects.in_week(wanted_week)
-    lesson_periods = lesson_periods.filter_from_type(type_, pk)
-
-    # Regroup lesson periods per weekday
-    per_period = {}
-    for lesson_period in lesson_periods:
-        added = False
-        if lesson_period.period.period in per_period:
-            if lesson_period.period.weekday in per_period[lesson_period.period.period]:
-                per_period[lesson_period.period.period][
-                    lesson_period.period.weekday
-                ].append(lesson_period)
-                added = True
-
-        if not added:
-            per_period.setdefault(lesson_period.period.period, {})[
-                lesson_period.period.weekday
-            ] = [lesson_period]
-
-    # Fill in empty lessons
-    for period_num in range(TimePeriod.period_min, TimePeriod.period_max + 1):
-        # Fill in empty weekdays
-        if period_num not in per_period.keys():
-            per_period[period_num] = {}
-
-        # Fill in empty lessons on this workday
-        for weekday_num in range(TimePeriod.weekday_min, TimePeriod.weekday_max + 1):
-            if weekday_num not in per_period[period_num].keys():
-                per_period[period_num][weekday_num] = []
-
-        # Order this weekday by periods
-        per_period[period_num] = OrderedDict(sorted(per_period[period_num].items()))
-
-    context["lesson_periods"] = OrderedDict(sorted(per_period.items()))
+    # Build timetable
+    timetable = build_timetable(type_, pk, wanted_week)
+    context["timetable"] = timetable
+
+    # Add time periods
     context["periods"] = TimePeriod.get_times_dict()
 
     # Build lists with weekdays and corresponding dates (long and short variant)
-    context["weekdays"] = [
-        (key, weekday, wanted_week[key])
-        for key, weekday in TimePeriod.WEEKDAY_CHOICES[
-            TimePeriod.weekday_min : TimePeriod.weekday_max + 1
-        ]
-    ]
-    context["weekdays_short"] = [
-        (key, weekday, wanted_week[key])
-        for key, weekday in TimePeriod.WEEKDAY_CHOICES_SHORT[
-            TimePeriod.weekday_min : TimePeriod.weekday_max + 1
-        ]
-    ]
+    context["weekdays"] = build_weekdays(TimePeriod.WEEKDAY_CHOICES, wanted_week)
+    context["weekdays_short"] = build_weekdays(TimePeriod.WEEKDAY_CHOICES_SHORT, wanted_week)
 
     context["weeks"] = get_weeks_for_year(year=wanted_week.year)
     context["week"] = wanted_week
@@ -322,12 +286,14 @@ def substitutions(
         day_contexts = {wanted_day: {"day": wanted_day}}
 
     for day in day_contexts:
-        subs = LessonSubstitution.objects.on_day(day).order_by("lesson_period__lesson__groups", "lesson_period__period")
+        subs = build_substitutions_list(day)
         day_contexts[day]["substitutions"] = subs
 
         day_contexts[day]["announcements"] = Announcement.for_timetables().on_date(day).filter(show_in_timetables=True)
 
         if config.CHRONOS_SUBSTITUTIONS_SHOW_HEADER_BOX:
+            subs = LessonSubstitution.objects.on_day(day).order_by("lesson_period__lesson__groups",
+                                                                   "lesson_period__period")
             day_contexts[day]["affected_teachers"] = subs.affected_teachers()
             day_contexts[day]["affected_groups"] = subs.affected_groups()