diff --git a/aleksis/apps/lesrooster/frontend/components/breaks_and_slots/Break.vue b/aleksis/apps/lesrooster/frontend/components/breaks_and_slots/Break.vue
index b1b6156eb677aa3d0fa98fdeb792ec5cd87a746b..8c39f2083ceb3dc41e13e871f54636e0f9c0847c 100644
--- a/aleksis/apps/lesrooster/frontend/components/breaks_and_slots/Break.vue
+++ b/aleksis/apps/lesrooster/frontend/components/breaks_and_slots/Break.vue
@@ -1,8 +1,7 @@
 <script>
 import {
   breakSlots,
-  createBreakSlot,
-  deleteBreakSlot,
+  createBreakSlots,
   deleteBreakSlots,
   updateBreakSlots,
 } from "./break.graphql";
@@ -39,15 +38,13 @@ export default {
       i18nKey: "lesrooster.break",
       createItemI18nKey: "lesrooster.break.create_item",
       gqlQuery: breakSlots,
-      gqlCreateMutation: createBreakSlot,
+      gqlCreateMutation: createBreakSlots,
       gqlPatchMutation: updateBreakSlots,
-      gqlDeleteMutation: deleteBreakSlot,
-      gqlDeleteMultipleMutation: deleteBreakSlots,
+      gqlDeleteMutation: deleteBreakSlots,
     };
   },
   methods: {
     getCreateData(item) {
-      console.log("in getCreateData", item);
       return {
         ...item,
         period: null,
@@ -55,17 +52,19 @@ export default {
         timeGrid: item.timeGrid.id,
       };
     },
-    getPatchData(items) {
-      console.log("patch items", items);
-      return items.map((item) => ({
+    getPatchData(item) {
+      item = {
         id: item.id,
         name: item.name,
-        weekday: this.weekdayAsInt(item.weekday),
+        weekday: item.weekday ? this.weekdayAsInt(item.weekday) : undefined,
         period: null,
         timeStart: item.timeStart,
         timeEnd: item.timeEnd,
-        timeGrid: item.timeGrid.id,
-      }));
+        timeGrid: item.timeGrid?.id,
+      };
+      return Object.fromEntries(
+        Object.entries(item).filter(([key, value]) => value !== undefined),
+      );
     },
   },
 };
diff --git a/aleksis/apps/lesrooster/frontend/components/breaks_and_slots/LesroosterSlot.vue b/aleksis/apps/lesrooster/frontend/components/breaks_and_slots/LesroosterSlot.vue
index 19189786fc653a67acfb24f6ad7b13a19e8c42ed..83da9f69c60148f2039591e2a5f5f9fd0b629992 100644
--- a/aleksis/apps/lesrooster/frontend/components/breaks_and_slots/LesroosterSlot.vue
+++ b/aleksis/apps/lesrooster/frontend/components/breaks_and_slots/LesroosterSlot.vue
@@ -15,7 +15,6 @@ import TimeGridField from "../validity_range/TimeGridField.vue";
     :gql-create-mutation="gqlCreateMutation"
     :gql-patch-mutation="gqlPatchMutation"
     :gql-delete-mutation="gqlDeleteMutation"
-    :gql-delete-multiple-mutation="gqlDeleteMultipleMutation"
     :default-item="defaultItem"
     :get-create-data="getCreateData"
     :get-patch-data="getPatchData"
@@ -114,13 +113,7 @@ import TimeGridField from "../validity_range/TimeGridField.vue";
 </template>
 
 <script>
-import {
-  slots,
-  createSlot,
-  deleteSlot,
-  deleteSlots,
-  updateSlots,
-} from "./slot.graphql";
+import { slots, createSlots, deleteSlots, updateSlots } from "./slot.graphql";
 
 export default {
   name: "LesroosterSlot",
@@ -156,10 +149,9 @@ export default {
       i18nKey: "lesrooster.slot",
       createItemI18nKey: "lesrooster.slot.create_slot",
       gqlQuery: slots,
-      gqlCreateMutation: createSlot,
+      gqlCreateMutation: createSlots,
       gqlPatchMutation: updateSlots,
-      gqlDeleteMutation: deleteSlot,
-      gqlDeleteMultipleMutation: deleteSlots,
+      gqlDeleteMutation: deleteSlots,
       defaultItem: {
         name: "",
         timeStart: "",
@@ -185,24 +177,26 @@ export default {
       return NaN;
     },
     getCreateData(item) {
-      console.log("in getCreateData", item);
       return {
         ...item,
         weekday: this.weekdayAsInt(item.weekday),
         timeGrid: item.timeGrid.id,
       };
     },
-    getPatchData(items) {
-      console.log("patch items", items);
-      return items.map((item) => ({
+    getPatchData(item) {
+      item = {
         id: item.id,
         name: item.name,
-        weekday: this.weekdayAsInt(item.weekday),
+        weekday: item.weekday ? this.weekdayAsInt(item.weekday) : undefined,
         period: item.period,
         timeStart: item.timeStart,
         timeEnd: item.timeEnd,
-        timeGrid: item.timeGrid.id,
-      }));
+        timeGrid: item.timeGrid?.id,
+      };
+      console.trace(item);
+      return Object.fromEntries(
+        Object.entries(item).filter(([key, value]) => value !== undefined),
+      );
     },
     formatTimeGrid(item) {
       if (!item) return null;
diff --git a/aleksis/apps/lesrooster/frontend/components/breaks_and_slots/break.graphql b/aleksis/apps/lesrooster/frontend/components/breaks_and_slots/break.graphql
index 63c2087d1f17c516c84ef8ac8e3fd84381fccd1c..324d35b3862627fa39a2362070a2b9ca8e1cfcb2 100644
--- a/aleksis/apps/lesrooster/frontend/components/breaks_and_slots/break.graphql
+++ b/aleksis/apps/lesrooster/frontend/components/breaks_and_slots/break.graphql
@@ -24,32 +24,6 @@ query breakSlots($orderBy: [String], $filters: JSONString) {
   }
 }
 
-mutation createBreakSlot($input: CreateBreakSlotInput!) {
-  createBreakSlot(input: $input) {
-    item: breakSlot {
-      id
-      name
-      timeGrid {
-        id
-        group {
-          id
-          name
-        }
-        validityRange {
-          id
-          name
-        }
-      }
-      weekday
-      period
-      timeStart
-      timeEnd
-      canEdit
-      canDelete
-    }
-  }
-}
-
 mutation createBreakSlots($input: [BatchCreateBreakSlotInput]!) {
   createBreakSlots(input: $input) {
     items: breakSlots {
@@ -77,12 +51,6 @@ mutation createBreakSlots($input: [BatchCreateBreakSlotInput]!) {
   }
 }
 
-mutation deleteBreakSlot($id: ID!) {
-  deleteBreakSlot(id: $id) {
-    ok
-  }
-}
-
 mutation deleteBreakSlots($ids: [ID]!) {
   deleteBreakSlots(ids: $ids) {
     deletionCount
@@ -90,7 +58,7 @@ mutation deleteBreakSlots($ids: [ID]!) {
 }
 
 mutation updateBreakSlots($input: [BatchPatchBreakSlotInput]!) {
-  batchMutation: updateBreakSlots(input: $input) {
+  updateBreakSlots(input: $input) {
     items: breakSlots {
       id
       name
diff --git a/aleksis/apps/lesrooster/frontend/components/breaks_and_slots/slot.graphql b/aleksis/apps/lesrooster/frontend/components/breaks_and_slots/slot.graphql
index 269faa53c9f3bb62ea2a5d138c778279089d433f..f9b8f08e21a95e867438e1852bc6819a051d84b8 100644
--- a/aleksis/apps/lesrooster/frontend/components/breaks_and_slots/slot.graphql
+++ b/aleksis/apps/lesrooster/frontend/components/breaks_and_slots/slot.graphql
@@ -23,32 +23,6 @@ query slots($orderBy: [String], $filters: JSONString) {
   }
 }
 
-mutation createSlot($input: CreateSlotInput!) {
-  createSlot(input: $input) {
-    item: slot {
-      id
-      name
-      timeGrid {
-        id
-        group {
-          id
-          name
-        }
-        validityRange {
-          id
-          name
-        }
-      }
-      weekday
-      period
-      timeStart
-      timeEnd
-      canEdit
-      canDelete
-    }
-  }
-}
-
 mutation createSlots($input: [BatchCreateSlotInput]!) {
   createSlots(input: $input) {
     items: slots {
@@ -76,12 +50,6 @@ mutation createSlots($input: [BatchCreateSlotInput]!) {
   }
 }
 
-mutation deleteSlot($id: ID!) {
-  deleteSlot(id: $id) {
-    ok
-  }
-}
-
 mutation deleteSlots($ids: [ID]!) {
   deleteSlots(ids: $ids) {
     deletionCount
@@ -89,7 +57,7 @@ mutation deleteSlots($ids: [ID]!) {
 }
 
 mutation updateSlots($input: [BatchPatchSlotInput]!) {
-  batchMutation: updateSlots(input: $input) {
+  updateSlots(input: $input) {
     items: slots {
       id
       name
diff --git a/aleksis/apps/lesrooster/frontend/components/lesson_raster/LessonRaster.vue b/aleksis/apps/lesrooster/frontend/components/lesson_raster/LessonRaster.vue
index 02860dc8c49c0a6a08136da9eb3749fe4e7dd50a..bc0205fe045c8ba045a7b13be511c7975158588d 100644
--- a/aleksis/apps/lesrooster/frontend/components/lesson_raster/LessonRaster.vue
+++ b/aleksis/apps/lesrooster/frontend/components/lesson_raster/LessonRaster.vue
@@ -171,37 +171,21 @@
       :disabled="
         $apollo.queries.items.loading || loading.main || loading[slot.weekday]
       "
-      @click:delete="deleteSingularSlot"
+      @click:delete="deleteSlot"
       @click:copy="copySingularSlotTodDay($event.item, $event.weekday)"
       :weekdays="weekdays"
       :id="'#slot-' + slot.id"
     />
 
     <delete-dialog
-      :gql-mutation="deleteMutation"
-      :gql-query="$apollo.queries.items"
-      v-model="deleteDialog"
-      :item="itemToDelete"
-    >
-      <template #body>
-        {{
-          $t(
-            "lesrooster." + itemToDelete.model.toLowerCase() + ".repr",
-            itemToDelete,
-          )
-        }}
-      </template>
-    </delete-dialog>
-
-    <delete-multiple-dialog
-      :gql-mutation="deleteMultipleMutation"
-      :gql-query="$apollo.queries.items"
+      :gql-delete-mutation="deleteMutation"
+      :affected-query="$apollo.queries.items"
       :items="itemsToDelete"
-      v-model="deleteMultipleDialog"
+      v-model="deleteDialog"
     >
       <template #title>
         {{
-          $t("lesrooster.slot.confirm_delete_multiple_slots", {
+          $t("lesrooster.slot.confirm_delete_slots", {
             day: $t("weekdays." + weekdayToDelete),
           })
         }}
@@ -214,7 +198,7 @@
           </li>
         </ul>
       </template>
-    </delete-multiple-dialog>
+    </delete-dialog>
   </div>
 </template>
 
@@ -223,11 +207,9 @@ import {
   carryOverSlots,
   copySlotsFromGrid,
   slots,
-  deleteSlot,
   deleteSlots,
 } from "../breaks_and_slots/slot.graphql";
 import DeleteDialog from "aleksis.core/components/generic/dialogs/DeleteDialog.vue";
-import DeleteMultipleDialog from "aleksis.core/components/generic/dialogs/DeleteMultipleDialog.vue";
 import CopyFromTimeGridMenu from "../validity_range/CopyFromTimeGridMenu.vue";
 import SlotCard from "./SlotCard.vue";
 import SlotCreator from "./SlotCreator.vue";
@@ -240,7 +222,6 @@ export default {
     CopyFromTimeGridMenu,
     SlotCreator,
     DeleteDialog,
-    DeleteMultipleDialog,
     SlotCard,
   },
   apollo: {
@@ -272,11 +253,8 @@ export default {
         main: false,
       },
       gqlQuery: slots,
-      deleteMutation: deleteSlot,
-      deleteMultipleMutation: deleteSlots,
+      deleteMutation: deleteSlots,
       deleteDialog: false,
-      deleteMultipleDialog: false,
-      itemToDelete: null,
       itemsToDelete: [],
       weekdayToDelete: "",
       createBreaks: false,
@@ -426,8 +404,8 @@ export default {
           this.loading[day] = false;
         });
     },
-    deleteSingularSlot(slot) {
-      this.itemToDelete = slot;
+    deleteSlot(slot) {
+      this.itemsToDelete = [slot];
       this.deleteDialog = true;
     },
     deleteSlotsOfDay(weekday) {
@@ -435,7 +413,7 @@ export default {
         (slot) => slot.weekday === weekday,
       );
       this.weekdayToDelete = weekday;
-      this.deleteMultipleDialog = true;
+      this.deleteDialog = true;
     },
     copyFromGrid(existingTimeGrid) {
       if (!this.internalTimeGrid || !this.internalTimeGrid.id) return;
diff --git a/aleksis/apps/lesrooster/frontend/components/lesson_raster/SlotCreator.vue b/aleksis/apps/lesrooster/frontend/components/lesson_raster/SlotCreator.vue
index bbf5d06bea283115f809cc6658fdc701854bdd62..f567120ee7a3b57756bd56896da930d48dd287f8 100644
--- a/aleksis/apps/lesrooster/frontend/components/lesson_raster/SlotCreator.vue
+++ b/aleksis/apps/lesrooster/frontend/components/lesson_raster/SlotCreator.vue
@@ -126,7 +126,7 @@ export default defineComponent({
     </template>
 
     <template #content>
-      <div aria-required="true">
+      <div v-if="!breaks" aria-required="true">
         <positive-small-integer-field
           v-model="slots.period"
           :label="$t('lesrooster.slot.period')"
diff --git a/aleksis/apps/lesrooster/frontend/components/supervision/Supervision.vue b/aleksis/apps/lesrooster/frontend/components/supervision/Supervision.vue
index 730c1bfcca44129f2d2a4e62e785979861da49ac..2a25319dab6e75a3540aa26b9b1e94517871c2fd 100644
--- a/aleksis/apps/lesrooster/frontend/components/supervision/Supervision.vue
+++ b/aleksis/apps/lesrooster/frontend/components/supervision/Supervision.vue
@@ -15,7 +15,6 @@ import InlineCRUDList from "aleksis.core/components/generic/InlineCRUDList.vue";
     :gql-create-mutation="gqlCreateMutation"
     :gql-patch-mutation="gqlPatchMutation"
     :gql-delete-mutation="gqlDeleteMutation"
-    :gql-delete-multiple-mutation="gqlDeleteMultipleMutation"
     :default-item="defaultItem"
     :get-create-data="getCreateData"
     :get-patch-data="getPatchData"
@@ -150,8 +149,7 @@ import InlineCRUDList from "aleksis.core/components/generic/InlineCRUDList.vue";
 <script>
 import {
   supervisions,
-  createSupervision,
-  deleteSupervision,
+  createSupervisions,
   deleteSupervisions,
   updateSupervisions,
 } from "./supervision.graphql";
@@ -161,7 +159,7 @@ import { rooms } from "aleksis.core/components/room/room.graphql";
 import { breakSlots } from "../breaks_and_slots/break.graphql";
 import {
   subjects,
-  createSubject,
+  createSubjects,
 } from "aleksis.apps.cursus/components/subject.graphql";
 
 import { RRule } from "rrule";
@@ -191,10 +189,9 @@ export default {
       i18nKey: "lesrooster.supervision",
       createItemI18nKey: "lesrooster.supervision.create_supervision",
       gqlQuery: supervisions,
-      gqlCreateMutation: createSupervision,
+      gqlCreateMutation: createSupervisions,
       gqlPatchMutation: updateSupervisions,
-      gqlDeleteMutation: deleteSupervision,
-      gqlDeleteMultipleMutation: deleteSupervisions,
+      gqlDeleteMutation: deleteSupervisions,
       defaultItem: {
         breakSlot: null,
         teachers: [],
@@ -202,7 +199,7 @@ export default {
       },
       subject: {
         gqlQuery: subjects,
-        gqlCreateMutation: createSubject,
+        gqlCreateMutation: createSubjects,
         transformCreateData(item) {
           return { ...item, parent: item.parent?.id };
         },
@@ -298,15 +295,18 @@ export default {
         recurrence: this.getRRule(item.breakSlot.timeGrid).toString(),
       };
     },
-    getPatchData(items) {
-      return items.map((item) => ({
+    getPatchData(item) {
+      item = {
         id: item.id,
-        breakSlot: item.breakSlot.id,
-        rooms: item.rooms.map((r) => r.id),
-        teachers: item.teachers.map((t) => t.id),
+        breakSlot: item.breakSlot?.id,
+        rooms: item.rooms?.map((r) => r.id),
+        teachers: item.teachers?.map((t) => t.id),
         subject: item.subject?.id,
         recurrence: this.getRRule(item.breakSlot.timeGrid).toString(),
-      }));
+      };
+      return Object.fromEntries(
+        Object.entries(item).filter(([key, value]) => value !== undefined),
+      );
     },
   },
 };
diff --git a/aleksis/apps/lesrooster/frontend/components/supervision/supervision.graphql b/aleksis/apps/lesrooster/frontend/components/supervision/supervision.graphql
index 26556dfe7a3abb474e64b04f19243df70e110770..12cda730c15f133992a90e36134e329fc210a5ca 100644
--- a/aleksis/apps/lesrooster/frontend/components/supervision/supervision.graphql
+++ b/aleksis/apps/lesrooster/frontend/components/supervision/supervision.graphql
@@ -47,14 +47,6 @@ query supervisions($orderBy: [String], $filters: JSONString) {
   }
 }
 
-mutation createSupervision($input: CreateSupervisionInput!) {
-  createSupervision(input: $input) {
-    item: supervision {
-      ...supervisionFields
-    }
-  }
-}
-
 mutation createSupervisions($input: [BatchCreateSupervisionInput]!) {
   createSupervisions(input: $input) {
     items: supervisions {
@@ -63,12 +55,6 @@ mutation createSupervisions($input: [BatchCreateSupervisionInput]!) {
   }
 }
 
-mutation deleteSupervision($id: ID!) {
-  deleteSupervision(id: $id) {
-    ok
-  }
-}
-
 mutation deleteSupervisions($ids: [ID]!) {
   deleteSupervisions(ids: $ids) {
     deletionCount
@@ -76,7 +62,7 @@ mutation deleteSupervisions($ids: [ID]!) {
 }
 
 mutation updateSupervisions($input: [BatchPatchSupervisionInput]!) {
-  batchMutation: updateSupervisions(input: $input) {
+  updateSupervisions(input: $input) {
     items: supervisions {
       ...supervisionFields
     }
diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigCRUDTable.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigCRUDTable.vue
index bf93ef62c6150e8610ad570b437a140f483d2b7c..927935eccdde231413106c0efef0b1ba55b57fa7 100644
--- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigCRUDTable.vue
+++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigCRUDTable.vue
@@ -136,8 +136,8 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue";
 <script>
 import {
   timeboundCourseConfigs,
-  createTimeboundCourseConfig,
-  deleteTimeboundCourseConfig,
+  createTimeboundCourseConfigs,
+  deleteTimeboundCourseConfigs,
   updateTimeboundCourseConfigs,
 } from "./timeboundCourseConfig.graphql";
 
@@ -172,9 +172,9 @@ export default {
       createItemI18nKey:
         "lesrooster.timebound_course_config.create_timebound_course_config",
       gqlQuery: timeboundCourseConfigs,
-      gqlCreateMutation: createTimeboundCourseConfig,
+      gqlCreateMutation: createTimeboundCourseConfigs,
       gqlPatchMutation: updateTimeboundCourseConfigs,
-      gqlDeleteMutation: deleteTimeboundCourseConfig,
+      gqlDeleteMutation: deleteTimeboundCourseConfigs,
       defaultItem: {
         course: {
           id: "",
@@ -192,7 +192,6 @@ export default {
   },
   methods: {
     getCreateData(item) {
-      console.log("in getCreateData", item);
       return {
         ...item,
         course: item.course.id,
@@ -200,15 +199,17 @@ export default {
         validityRange: item.validityRange.id,
       };
     },
-    getPatchData(items) {
-      console.log("patch items", items);
-      return items.map((item) => ({
+    getPatchData(item) {
+      item = {
         id: item.id,
-        course: item.course.id,
-        teachers: item.teachers.map((t) => t.id),
-        validityRange: item.validityRange.id,
+        course: item.course?.id,
+        teachers: item.teachers?.map((t) => t.id),
+        validityRange: item.validityRange?.id,
         lessonQuota: item.lessonQuota,
-      }));
+      };
+      return Object.fromEntries(
+        Object.entries(item).filter(([key, value]) => value !== undefined),
+      );
     },
   },
   apollo: {
diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue
index 7a0a700a9820ae1453204db37abab6f804448b7b..79715008b5a2e9c0616de9c65ebacdb46a664463 100644
--- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue
+++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/TimeboundCourseConfigRaster.vue
@@ -188,7 +188,7 @@ import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue";
 <script>
 import {
   subjects,
-  batchCreateTimeboundCourseConfig,
+  createTimeboundCourseConfigs,
   updateTimeboundCourseConfigs,
 } from "./timeboundCourseConfig.graphql";
 
@@ -196,7 +196,7 @@ import { currentValidityRange as gqlCurrentValidityRange } from "../validity_ran
 
 import { gqlGroups, gqlTeachers } from "../helper.graphql";
 
-import { batchCreateCourse } from "aleksis.apps.cursus/components/course.graphql";
+import { createCourses } from "aleksis.apps.cursus/components/course.graphql";
 
 export default {
   name: "TimeboungCourseConfigRaster",
@@ -269,7 +269,7 @@ export default {
         }
       } else {
         if (
-          !course.lrTimeboundCourseConfigs.filter(
+          !course.lrTimeboundCourseConfigs?.filter(
             (c) => c.validityRange.id === this.internalValidityRange?.id,
           ).length
         ) {
@@ -312,11 +312,11 @@ export default {
         },
         {
           data: this.createdCourseConfigs,
-          mutation: batchCreateTimeboundCourseConfig,
+          mutation: createTimeboundCourseConfigs,
         },
         {
           data: this.createdCourses,
-          mutation: batchCreateCourse,
+          mutation: createCourses,
         },
       ]) {
         if (mutationCombination.data.length) {
diff --git a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql
index 9ede745ed2fff96245478ed7871745a751cadf58..885c6d3349c978c90bc70f49278de5799c26dec4 100644
--- a/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql
+++ b/aleksis/apps/lesrooster/frontend/components/timebound_course_config/timeboundCourseConfig.graphql
@@ -55,26 +55,10 @@ query timeboundCourseConfigs($orderBy: [String], $filters: JSONString) {
   }
 }
 
-mutation createTimeboundCourseConfig(
-  $input: CreateTimeboundCourseConfigInput!
-) {
-  createTimeboundCourseConfig(input: $input) {
-    item: timeboundCourseConfig {
-      ...timeboundCourseConfigFields
-      course {
-        ...courseFields
-        subject {
-          ...subjectFields
-        }
-      }
-    }
-  }
-}
-
-mutation batchCreateTimeboundCourseConfig(
+mutation createTimeboundCourseConfigs(
   $input: [BatchCreateTimeboundCourseConfigInput]!
 ) {
-  batchCreateTimeboundCourseConfig(input: $input) {
+  createTimeboundCourseConfigs(input: $input) {
     item: timeboundCourseConfigs {
       ...timeboundCourseConfigFields
       course {
@@ -87,16 +71,16 @@ mutation batchCreateTimeboundCourseConfig(
   }
 }
 
-mutation deleteTimeboundCourseConfig($id: ID!) {
-  deleteTimeboundCourseConfig(id: $id) {
-    ok
+mutation deleteTimeboundCourseConfigs($ids: [ID]!) {
+  deleteTimeboundCourseConfigs(ids: $ids) {
+    deletionCount
   }
 }
 
 mutation updateTimeboundCourseConfigs(
   $input: [BatchPatchTimeboundCourseConfigInput]!
 ) {
-  batchMutation: updateTimeboundCourseConfigs(input: $input) {
+  updateTimeboundCourseConfigs(input: $input) {
     items: timeboundCourseConfigs {
       ...timeboundCourseConfigFields
       course {
diff --git a/aleksis/apps/lesrooster/frontend/components/timetable_management/TimetableManagement.vue b/aleksis/apps/lesrooster/frontend/components/timetable_management/TimetableManagement.vue
index 9228348857cc5006068248fe38971666a1d0bdbe..1fbc0d46d7844cf143a92476de933c30f9ff3d7b 100644
--- a/aleksis/apps/lesrooster/frontend/components/timetable_management/TimetableManagement.vue
+++ b/aleksis/apps/lesrooster/frontend/components/timetable_management/TimetableManagement.vue
@@ -2,13 +2,13 @@
 import { defineComponent } from "vue";
 import {
   courses,
-  createLesson,
-  deleteLesson,
+  createLessons,
+  deleteLessons,
   gqlGroups,
   lessonObjects,
   moveLesson,
   overlayLessons,
-  updateLesson,
+  updateLessons,
 } from "./timetableManagement.graphql";
 import { gqlTeachers } from "../helper.graphql";
 import { timeGrids } from "../validity_range/validityRange.graphql";
@@ -56,14 +56,15 @@ export default defineComponent({
       courseSearch: null,
       lessonsUsed: {},
       lessonQuotaTotal: 0,
-      deleteMutation: deleteLesson,
+      deleteMutation: deleteLessons,
       deleteDialog: false,
-      itemToDelete: null,
+      itemsToDelete: [],
       selectedObject: null,
       selectedObjectType: null,
       selectedObjectTitle: "",
       selectedObjectDialogOpen: false,
-      selectedObjectDialogTab: null,
+      selectedObjectDialogTab: 0,
+      timeGrids: [],
       groups: [],
       selectedGroup: null,
       lessonEdit: {
@@ -90,7 +91,7 @@ export default defineComponent({
             value: "rooms",
           },
         ],
-        mutation: updateLesson,
+        mutation: updateLessons,
       },
       draggedItem: null,
       overlayLessons: [],
@@ -233,7 +234,12 @@ export default defineComponent({
   },
   computed: {
     readyForQueries() {
-      return this.internalTimeGrid !== null && this.selectedGroup !== null;
+      // Non-typesafe check to also handle undefined
+      return (
+        this.internalTimeGrid != null &&
+        this.selectedGroup != null &&
+        this.selectedGroup.id != null
+      );
     },
     lessons() {
       return this.lessonObjects
@@ -385,28 +391,30 @@ export default defineComponent({
           .mutate({
             mutation: moveLesson,
             variables: {
-              id: eventData.data.id,
               input: {
+                id: eventData.data.id,
                 slotStart: newStartSlotId,
                 slotEnd: newEndSlotId,
               },
             },
             optimisticResponse: {
-              updateLesson: {
-                lesson: {
-                  ...eventData.data,
-                  slotStart: newStartSlot,
-                  slotEnd: newEndSlot,
-                  isOptimistic: true,
-                },
-                __typename: "LessonPatchMutation",
+              updateLessons: {
+                lessons: [
+                  {
+                    ...eventData.data,
+                    slotStart: newStartSlot,
+                    slotEnd: newEndSlot,
+                    isOptimistic: true,
+                  },
+                ],
+                __typename: "LessonBatchPatchMutation",
               },
             },
             update(
               store,
               {
                 data: {
-                  updateLesson: { lesson },
+                  updateLessons: { lessons },
                 },
               },
             ) {
@@ -424,14 +432,16 @@ export default defineComponent({
                 return;
               }
 
-              const index = storedData.lessonObjects.findIndex(
-                (lessonObject) => lessonObject.id === lesson.id,
-              );
-              storedData.lessonObjects[index].slotStart = lesson.slotStart;
-              storedData.lessonObjects[index].slotEnd = lesson.slotEnd;
+              lessons.forEach((lesson) => {
+                const index = storedData.lessonObjects.findIndex(
+                  (lessonObject) => lessonObject.id === lesson.id,
+                );
+                storedData.lessonObjects[index].slotStart = lesson.slotStart;
+                storedData.lessonObjects[index].slotEnd = lesson.slotEnd;
 
-              // Write data back to the cache
-              store.writeQuery({ ...query, data: storedData });
+                // Write data back to the cache
+                store.writeQuery({ ...query, data: storedData });
+              });
             },
           })
           .then(() => {
@@ -454,7 +464,7 @@ export default defineComponent({
         const recurrenceString = rule.toString();
         this.$apollo
           .mutate({
-            mutation: createLesson,
+            mutation: createLessons,
             variables: {
               input: {
                 slotStart: newStartSlotId,
@@ -468,30 +478,32 @@ export default defineComponent({
               },
             },
             optimisticResponse: {
-              createLesson: {
-                lesson: {
-                  id: "temporary-lesson-id-" + crypto.randomUUID(),
-                  slotStart: newStartSlot,
-                  slotEnd: newEndSlot,
-                  subject: eventData.data.subject,
-                  teachers: eventData.data.teachers,
-                  // rooms: eventData.data.rooms,
-                  rooms: [],
-                  course: eventData.data,
-                  isOptimistic: true,
-                  canEdit: true,
-                  canDelete: true,
-                  recurrence: recurrenceString,
-                  __typename: "LessonType",
-                },
-                __typename: "LessonCreateMutation",
+              createLessons: {
+                items: [
+                  {
+                    id: "temporary-lesson-id-" + crypto.randomUUID(),
+                    slotStart: newStartSlot,
+                    slotEnd: newEndSlot,
+                    subject: eventData.data.subject,
+                    teachers: eventData.data.teachers,
+                    // rooms: eventData.data.rooms,
+                    rooms: [],
+                    course: eventData.data,
+                    isOptimistic: true,
+                    canEdit: true,
+                    canDelete: true,
+                    recurrence: recurrenceString,
+                    __typename: "LessonType",
+                  },
+                ],
+                __typename: "LessonBatchCreateMutation",
               },
             },
             update(
               store,
               {
                 data: {
-                  createLesson: { lesson },
+                  createLessons: { items },
                 },
               },
             ) {
@@ -509,7 +521,7 @@ export default defineComponent({
                 return;
               }
 
-              storedData.lessonObjects.push(lesson);
+              items.forEach((lesson) => storedData.lessonObjects.push(lesson));
 
               // Write data back to the cache
               store.writeQuery({ ...query, data: storedData });
@@ -558,28 +570,30 @@ export default defineComponent({
         .mutate({
           mutation: moveLesson,
           variables: {
-            id: lesson.id,
             input: {
+              id: lesson.id,
               slotStart: slotStart.id,
               slotEnd: slotEnd.id,
             },
           },
           optimisticResponse: {
-            updateLesson: {
-              lesson: {
-                ...lesson,
-                slotStart: slotStart,
-                slotEnd: slotEnd,
-                isOptimistic: true,
-              },
-              __typename: "LessonPatchMutation",
+            updateLessons: {
+              lessons: [
+                {
+                  ...lesson,
+                  slotStart: slotStart,
+                  slotEnd: slotEnd,
+                  isOptimistic: true,
+                },
+              ],
+              __typename: "LessonBatchPatchMutation",
             },
           },
           update(
             store,
             {
               data: {
-                updateLesson: { lesson },
+                updateLessons: { lessons },
               },
             },
           ) {
@@ -597,14 +611,16 @@ export default defineComponent({
               return;
             }
 
-            const index = storedData.lessonObjects.findIndex(
-              (lessonObject) => lessonObject.id === lesson.id,
-            );
-            storedData.lessonObjects[index].slotStart = lesson.slotStart;
-            storedData.lessonObjects[index].slotEnd = lesson.slotEnd;
+            lessons.forEach((lesson) => {
+              const index = storedData.lessonObjects.findIndex(
+                (lessonObject) => lessonObject.id === lesson.id,
+              );
+              storedData.lessonObjects[index].slotStart = lesson.slotStart;
+              storedData.lessonObjects[index].slotEnd = lesson.slotEnd;
 
-            // Write data back to the cache
-            store.writeQuery({ ...query, data: storedData });
+              // Write data back to the cache
+              store.writeQuery({ ...query, data: storedData });
+            });
           },
         })
         .then(() => {
@@ -647,7 +663,7 @@ export default defineComponent({
       this.changeLessonSlots(lesson, lesson.slotStart, slotEnd);
     },
     deleteLesson(lesson) {
-      this.itemToDelete = lesson;
+      this.itemsToDelete = [lesson];
       this.deleteDialog = true;
     },
     teacherClick(teacher) {
@@ -705,6 +721,11 @@ export default defineComponent({
       const index = storedData.lessonObjects.findIndex(
         (lessonObject) => lessonObject.id === lesson.id,
       );
+
+      if (index === -1) {
+        return;
+      }
+
       storedData.lessonObjects[index].subject = lesson.subject;
       storedData.lessonObjects[index].teachers = lesson.teachers;
       storedData.lessonObjects[index].rooms = lesson.rooms;
@@ -724,6 +745,7 @@ export default defineComponent({
     },
     lessonEditGetPatchData(lesson) {
       return {
+        id: lesson.id,
         subject: lesson.subject.id,
         teachers: lesson.teachers.map((teacher) => teacher.id),
         rooms: lesson.rooms.map((room) => room.id),
@@ -867,7 +889,7 @@ export default defineComponent({
         <div id="periods">
           <period-card
             v-for="(period, index) in periods"
-            :key="period"
+            :key="'period-' + period"
             :period="period"
             :weekdays="weekdays"
             :time-ranges="
@@ -969,7 +991,7 @@ export default defineComponent({
               :periods="periods"
               :weekdays="weekdays"
               :lesson="overlayLesson"
-              :key="overlayLesson.id"
+              :key="'overlay-' + overlayLesson.id"
             />
           </template>
         </drag-grid>
@@ -1056,14 +1078,20 @@ export default defineComponent({
             v-if="timeGrids && timeGrids.length > 1"
             class="width-max-content"
           >
-            <v-tab v-for="timeGrid in timeGrids" :key="timeGrid.id">
+            <v-tab
+              v-for="timeGrid in timeGrids"
+              :key="'tabSelector-' + timeGrid.id"
+            >
               {{ formatTimeGrid(timeGrid) }}
             </v-tab>
           </v-tabs>
         </v-card-title>
         <v-card-text>
           <v-tabs-items v-model="selectedObjectDialogTab">
-            <v-tab-item v-for="timeGrid in timeGrids" :key="timeGrid.id">
+            <v-tab-item
+              v-for="timeGrid in timeGrids"
+              :key="'tabItem-' + timeGrid.id"
+            >
               <teacher-time-table
                 v-if="internalTimeGrid && selectedObjectType === 'teacher'"
                 :teacher-id="selectedObject"
@@ -1083,13 +1111,17 @@ export default defineComponent({
     </mobile-fullscreen-dialog>
 
     <delete-dialog
-      :gql-mutation="deleteMutation"
-      :gql-query="$apollo.queries.lessonObjects"
+      :gql-delete-mutation="deleteMutation"
+      :affected-query="$apollo.queries.lessonObjects"
       v-model="deleteDialog"
-      :item="itemToDelete"
+      :items="itemsToDelete"
     >
       <template #body>
-        {{ itemToDelete.subject?.name || itemToDelete.course.subject.name }}
+        <ul class="text-body-1">
+          <li v-for="item in itemsToDelete" :key="'delete-' + item.id">
+            {{ item.subject?.name || item.course.subject.name }}
+          </li>
+        </ul>
       </template>
     </delete-dialog>
 
diff --git a/aleksis/apps/lesrooster/frontend/components/timetable_management/TimetableOverlayCard.vue b/aleksis/apps/lesrooster/frontend/components/timetable_management/TimetableOverlayCard.vue
index 8d05ecb667021940d1223bd646c4d4ab9dfbf36b..92de1150203ed4deeca5a68324b22b53953cd96d 100644
--- a/aleksis/apps/lesrooster/frontend/components/timetable_management/TimetableOverlayCard.vue
+++ b/aleksis/apps/lesrooster/frontend/components/timetable_management/TimetableOverlayCard.vue
@@ -45,9 +45,10 @@ export default {
       );
     },
     rooms() {
-      return this.lesson.rooms.filter(
+      // dragged item may be a course which doesn't have a field rooms
+      return this.lesson.rooms?.filter(
         (lessonRoom) =>
-          !!this.draggedItem.rooms.find((room) => room.id === lessonRoom.id),
+          !!this.draggedItem.rooms?.find((room) => room.id === lessonRoom.id),
       );
     },
     teachers() {
@@ -68,7 +69,7 @@ export default {
       v-for="room in rooms"
       icon="mdi-home-off-outline"
       color="warning"
-      :key="room.id"
+      :key="'room-' + room.id"
     >
       <colored-short-name-chip class="short" :item="room" :elevation="0" />
     </blocking-card>
@@ -76,7 +77,7 @@ export default {
       v-for="teacher in teachers"
       icon="mdi-account-off-outline"
       color="warning"
-      :key="teacher.id"
+      :key="'teacher-' + teacher.id"
     >
       <colored-short-name-chip class="short" :item="teacher" :elevation="0" />
     </blocking-card>
diff --git a/aleksis/apps/lesrooster/frontend/components/timetable_management/timetableManagement.graphql b/aleksis/apps/lesrooster/frontend/components/timetable_management/timetableManagement.graphql
index 970c1c0be810fd48706f88722dc503063acfbdac..8f29aa75315e02433be28051c860bed2990cdcdd 100644
--- a/aleksis/apps/lesrooster/frontend/components/timetable_management/timetableManagement.graphql
+++ b/aleksis/apps/lesrooster/frontend/components/timetable_management/timetableManagement.graphql
@@ -138,9 +138,9 @@ query overlayLessons($rooms: [ID]!, $teachers: [ID]!, $timeGrid: ID!) {
   }
 }
 
-mutation createLesson($input: CreateLessonInput!) {
-  createLesson(input: $input) {
-    lesson {
+mutation createLessons($input: [BatchCreateLessonInput]!) {
+  createLessons(input: $input) {
+    items: lessons {
       id
       slotStart {
         id
@@ -198,9 +198,9 @@ mutation createLesson($input: CreateLessonInput!) {
   }
 }
 
-mutation moveLesson($id: ID!, $input: PatchLessonInput!) {
-  updateLesson(id: $id, input: $input) {
-    lesson {
+mutation moveLesson($input: [BatchPatchLessonInput]!) {
+  updateLessons(input: $input) {
+    lessons {
       id
       slotStart {
         id
@@ -217,9 +217,9 @@ mutation moveLesson($id: ID!, $input: PatchLessonInput!) {
   }
 }
 
-mutation updateLesson($input: PatchLessonInput!, $id: ID!) {
-  updateLesson(id: $id, input: $input) {
-    item: lesson {
+mutation updateLessons($input: [BatchPatchLessonInput]!) {
+  updateLessons(input: $input) {
+    items: lessons {
       id
       subject {
         id
@@ -245,8 +245,8 @@ mutation updateLesson($input: PatchLessonInput!, $id: ID!) {
   }
 }
 
-mutation deleteLesson($id: ID!) {
-  deleteLesson(id: $id) {
-    ok
+mutation deleteLessons($ids: [ID]!) {
+  deleteLessons(ids: $ids) {
+    deletionCount
   }
 }
diff --git a/aleksis/apps/lesrooster/frontend/components/validity_range/TimeGridField.vue b/aleksis/apps/lesrooster/frontend/components/validity_range/TimeGridField.vue
index 033528208a9418de5fb573cf57d59c929caed30f..69246ca805cd43a8995de9088ef29454d2ed4e3e 100644
--- a/aleksis/apps/lesrooster/frontend/components/validity_range/TimeGridField.vue
+++ b/aleksis/apps/lesrooster/frontend/components/validity_range/TimeGridField.vue
@@ -5,7 +5,7 @@ import ValidityRangeField from "./ValidityRangeField.vue";
 
 <script>
 import { defineComponent } from "vue";
-import { timeGrids, createTimeGrid } from "./validityRange.graphql";
+import { timeGrids, createTimeGrids } from "./validityRange.graphql";
 import { gqlGroups } from "../helper.graphql";
 
 export default defineComponent({
@@ -38,7 +38,7 @@ export default defineComponent({
       ],
       i18nKey: "lesrooster.validity_range.time_grid",
       gqlQuery: timeGrids,
-      gqlCreateMutation: createTimeGrid,
+      gqlCreateMutation: createTimeGrids,
       defaultItem: {
         isGeneric: false,
         group: null,
@@ -63,7 +63,7 @@ export default defineComponent({
         (group) =>
           !this.$refs.field.items.some(
             (timeGrid) =>
-              timeGrid.validityRange.id === itemModel.validityRange.id &&
+              timeGrid.validityRange.id === itemModel.validityRange?.id &&
               timeGrid.group !== null &&
               timeGrid.group.id === group.id,
           ),
@@ -75,7 +75,7 @@ export default defineComponent({
       // Is there a timeGrid that has the same validityRange as we and no group?
       return this.$refs.field.items.some(
         (timeGrid) =>
-          timeGrid.validityRange.id === itemModel.validityRange.id &&
+          timeGrid.validityRange.id === itemModel.validityRange?.id &&
           timeGrid.group === null,
       );
     },
diff --git a/aleksis/apps/lesrooster/frontend/components/validity_range/ValidityRange.vue b/aleksis/apps/lesrooster/frontend/components/validity_range/ValidityRange.vue
index b203b9d77cfe8a88eb771f23ee928270dc385f15..52a8156217b86c1f6bae0e71449681e4125c3693 100644
--- a/aleksis/apps/lesrooster/frontend/components/validity_range/ValidityRange.vue
+++ b/aleksis/apps/lesrooster/frontend/components/validity_range/ValidityRange.vue
@@ -21,7 +21,6 @@ import ValidityRangeStatusChip from "./ValidityRangeStatusChip.vue";
       :gql-create-mutation="gqlCreateMutation"
       :gql-patch-mutation="gqlPatchMutation"
       :gql-delete-mutation="gqlDeleteMutation"
-      :gql-delete-multiple-mutation="gqlDeleteMultipleMutation"
       :default-item="defaultItem"
       :get-create-data="getCreateData"
       :get-patch-data="getPatchData"
@@ -210,12 +209,11 @@ import ValidityRangeStatusChip from "./ValidityRangeStatusChip.vue";
 <script>
 import {
   validityRanges,
-  createValidityRange,
-  deleteValidityRange,
+  createValidityRanges,
   deleteValidityRanges,
   updateValidityRanges,
-  createTimeGrid,
-  deleteTimeGrid,
+  createTimeGrids,
+  deleteTimeGrids,
 } from "./validityRange.graphql";
 import { gqlGroups } from "../helper.graphql";
 
@@ -253,10 +251,9 @@ export default {
       ],
       i18nKey: "lesrooster.validity_range",
       gqlQuery: validityRanges,
-      gqlCreateMutation: createValidityRange,
+      gqlCreateMutation: createValidityRanges,
       gqlPatchMutation: updateValidityRanges,
-      gqlDeleteMutation: deleteValidityRange,
-      gqlDeleteMultipleMutation: deleteValidityRanges,
+      gqlDeleteMutation: deleteValidityRanges,
       defaultItem: {
         name: "",
         dateStart: "",
@@ -268,13 +265,13 @@ export default {
         open: false,
         deleteOpen: false,
         deleteItem: null,
-        deleteMutation: deleteTimeGrid,
+        deleteMutation: deleteTimeGrids,
         range: null,
         object: {
           isGeneric: false,
           group: null,
         },
-        mutation: createTimeGrid,
+        mutation: createTimeGrids,
         fields: [
           {
             text: this.$t(
@@ -313,22 +310,23 @@ export default {
   },
   methods: {
     getCreateData(item) {
-      console.log("in getCreateData", item);
       return {
         ...item,
         schoolTerm: item.schoolTerm?.id,
       };
     },
-    getPatchData(items) {
-      console.log("patch items", items);
-      return items.map((item) => ({
+    getPatchData(item) {
+      item = {
         id: item.id,
         name: item.name,
         dateStart: item.dateStart,
         dateEnd: item.dateEnd,
-        schoolTerm: item.schoolTerm.id,
-        status: item.status.toLowerCase(),
-      }));
+        schoolTerm: item.schoolTerm?.id,
+        status: item.status?.toLowerCase(),
+      };
+      return Object.fromEntries(
+        Object.entries(item).filter(([key, value]) => value !== undefined),
+      );
     },
     createTimeGridFor(validityRange) {
       this.timeGrids.range = validityRange;
diff --git a/aleksis/apps/lesrooster/frontend/components/validity_range/ValidityRangeField.vue b/aleksis/apps/lesrooster/frontend/components/validity_range/ValidityRangeField.vue
index 274df9db207462cf7d81edff2f94b7350e5cfd56..72ee632639fb6293d583e88d6297cc8896aebcd5 100644
--- a/aleksis/apps/lesrooster/frontend/components/validity_range/ValidityRangeField.vue
+++ b/aleksis/apps/lesrooster/frontend/components/validity_range/ValidityRangeField.vue
@@ -59,7 +59,7 @@ import SchoolTermField from "aleksis.core/components/school_term/SchoolTermField
 </template>
 
 <script>
-import { validityRanges, createValidityRange } from "./validityRange.graphql";
+import { validityRanges, createValidityRanges } from "./validityRange.graphql";
 
 export default {
   name: "ValidityRangeField",
@@ -85,7 +85,7 @@ export default {
       ],
       i18nKey: "lesrooster.validity_range",
       gqlQuery: validityRanges,
-      gqlCreateMutation: createValidityRange,
+      gqlCreateMutation: createValidityRanges,
       defaultItem: {
         name: "",
         dateStart: "",
@@ -97,21 +97,22 @@ export default {
   },
   methods: {
     getCreateData(item) {
-      console.log("in getCreateData", item);
       return {
         ...item,
         schoolTerm: item.schoolTerm?.id,
       };
     },
-    getPatchData(items) {
-      console.log("patch items", items);
-      return items.map((item) => ({
+    getPatchData(item) {
+      item = {
         id: item.id,
         name: item.name,
         dateStart: item.dateStart,
         dateEnd: item.dateEnd,
         schoolTerm: item.schoolTerm.id,
-      }));
+      };
+      return Object.fromEntries(
+        Object.entries(item).filter(([key, value]) => value !== undefined),
+      );
     },
   },
 };
diff --git a/aleksis/apps/lesrooster/frontend/components/validity_range/validityRange.graphql b/aleksis/apps/lesrooster/frontend/components/validity_range/validityRange.graphql
index 5c15b259d0c77e832b3b11cce0c27c1d57be05ed..7124e1491f40ef9638f84e984f72f287fb04fcb1 100644
--- a/aleksis/apps/lesrooster/frontend/components/validity_range/validityRange.graphql
+++ b/aleksis/apps/lesrooster/frontend/components/validity_range/validityRange.graphql
@@ -22,9 +22,9 @@ query validityRanges($orderBy: [String], $filters: JSONString) {
   }
 }
 
-mutation createValidityRange($input: CreateValidityRangeInput!) {
-  createValidityRange(input: $input) {
-    item: validityRange {
+mutation createValidityRanges($input: [BatchCreateValidityRangeInput]!) {
+  createValidityRanges(input: $input) {
+    items: validityRanges {
       id
       name
       status
@@ -48,12 +48,6 @@ mutation createValidityRange($input: CreateValidityRangeInput!) {
   }
 }
 
-mutation deleteValidityRange($id: ID!) {
-  deleteValidityRange(id: $id) {
-    ok
-  }
-}
-
 mutation deleteValidityRanges($ids: [ID]!) {
   deleteValidityRanges(ids: $ids) {
     deletionCount
@@ -61,7 +55,7 @@ mutation deleteValidityRanges($ids: [ID]!) {
 }
 
 mutation updateValidityRanges($input: [BatchPatchValidityRangeInput]!) {
-  batchMutation: updateValidityRanges(input: $input) {
+  utation: updateValidityRanges(input: $input) {
     items: validityRanges {
       id
       name
@@ -95,9 +89,9 @@ query currentValidityRange {
   }
 }
 
-mutation createTimeGrid($input: CreateTimeGridInput!) {
-  createTimeGrid(input: $input) {
-    item: timeGrid {
+mutation createTimeGrids($input: [BatchCreateTimeGridInput]!) {
+  createTimeGrids(input: $input) {
+    items: timeGrids {
       id
       group {
         id
@@ -111,9 +105,9 @@ mutation createTimeGrid($input: CreateTimeGridInput!) {
   }
 }
 
-mutation deleteTimeGrid($id: ID!) {
-  deleteTimeGrid(id: $id) {
-    ok
+mutation deleteTimeGrids($ids: [ID]!) {
+  deleteTimeGrids(id: $ids) {
+    deletionCount
   }
 }
 
diff --git a/aleksis/apps/lesrooster/frontend/index.js b/aleksis/apps/lesrooster/frontend/index.js
index 1a82aec7706253dd24cc8dd41d0f29c0995abf73..5f072d5659068ce601a3a287c27a2d5b68ae7dbc 100644
--- a/aleksis/apps/lesrooster/frontend/index.js
+++ b/aleksis/apps/lesrooster/frontend/index.js
@@ -58,6 +58,7 @@ export default {
         toolbarTitle: "lesrooster.timetable_management.menu_title",
         icon: "mdi-magnet",
         permission: "lesrooster.plan_timetables_rule",
+        fullWidth: true,
       },
       children: [
         {
@@ -67,7 +68,10 @@ export default {
           name: "lesrooster.timetable_management",
           props: true,
           meta: {
+            titleKey: "lesrooster.timetable_management.menu_title",
+            toolbarTitle: "lesrooster.timetable_management.menu_title",
             permission: "lesrooster.plan_timetables_rule",
+            fullWidth: true,
           },
         },
       ],
diff --git a/aleksis/apps/lesrooster/schema/__init__.py b/aleksis/apps/lesrooster/schema/__init__.py
index ca3170100b2298fdbc454068d1d5ab858bbc3f75..d27e9288e94c596457496694b6955ba80cd74608 100644
--- a/aleksis/apps/lesrooster/schema/__init__.py
+++ b/aleksis/apps/lesrooster/schema/__init__.py
@@ -24,16 +24,12 @@ from .break_slot import (
     BreakSlotBatchCreateMutation,
     BreakSlotBatchDeleteMutation,
     BreakSlotBatchPatchMutation,
-    BreakSlotCreateMutation,
-    BreakSlotDeleteMutation,
     BreakSlotType,
 )
 from .lesson import (
+    LessonBatchCreateMutation,
     LessonBatchDeleteMutation,
     LessonBatchPatchMutation,
-    LessonCreateMutation,
-    LessonDeleteMutation,
-    LessonPatchMutation,
     LessonType,
 )
 from .slot import (
@@ -42,36 +38,30 @@ from .slot import (
     SlotBatchCreateMutation,
     SlotBatchDeleteMutation,
     SlotBatchPatchMutation,
-    SlotCreateMutation,
-    SlotDeleteMutation,
     SlotType,
 )
 from .supervision import (
+    SupervisionBatchCreateMutation,
     SupervisionBatchDeleteMutation,
     SupervisionBatchPatchMutation,
-    SupervisionCreateMutation,
-    SupervisionDeleteMutation,
     SupervisionType,
 )
 from .time_grid import (
+    TimeGridBatchCreateMutation,
     TimeGridBatchDeleteMutation,
-    TimeGridCreateMutation,
-    TimeGridDeleteMutation,
     TimeGridType,
 )
 from .timebound_course_config import (
     LesroosterExtendedSubjectType,
     TimeboundCourseConfigBatchCreateMutation,
+    TimeboundCourseConfigBatchDeleteMutation,
     TimeboundCourseConfigBatchPatchMutation,
-    TimeboundCourseConfigCreateMutation,
-    TimeboundCourseConfigDeleteMutation,
     TimeboundCourseConfigType,
 )
 from .validity_range import (
+    ValidityRangeBatchCreateMutation,
     ValidityRangeBatchDeleteMutation,
     ValidityRangeBatchPatchMutation,
-    ValidityRangeCreateMutation,
-    ValidityRangeDeleteMutation,
     ValidityRangeType,
 )
 
@@ -234,7 +224,7 @@ class Query(graphene.ObjectType):
             Q(teachers=teacher) | Q(course__teachers=teacher),
             slot_start__time_grid_id=time_grid,
             slot_end__time_grid_id=time_grid,
-        )
+        ).distinct()
 
     @staticmethod
     def resolve_lesson_objects_for_room(root, info, room, time_grid):
@@ -245,7 +235,7 @@ class Query(graphene.ObjectType):
             rooms=room,
             slot_start__time_grid_id=time_grid,
             slot_end__time_grid_id=time_grid,
-        )
+        ).distinct()
 
     @staticmethod
     def resolve_lessons_objects_for_rooms_or_teachers(
@@ -262,7 +252,7 @@ class Query(graphene.ObjectType):
             Q(rooms__in=rooms) | Q(teachers__in=teachers) | Q(course__teachers__in=teachers),
             slot_start__time_grid_id=time_grid,
             slot_end__time_grid_id=time_grid,
-        )
+        ).distinct()
 
     @staticmethod
     def resolve_groups_by_time_grid(root, info, time_grid=None, **kwargs):
@@ -277,42 +267,32 @@ class Query(graphene.ObjectType):
 
 
 class Mutation(graphene.ObjectType):
-    create_break_slot = BreakSlotCreateMutation.Field()
     create_break_slots = BreakSlotBatchCreateMutation.Field()
-    delete_break_slot = BreakSlotDeleteMutation.Field()
     delete_break_slots = BreakSlotBatchDeleteMutation.Field()
     update_break_slots = BreakSlotBatchPatchMutation.Field()
 
-    create_slot = SlotCreateMutation.Field()
     create_slots = SlotBatchCreateMutation.Field()
-    delete_slot = SlotDeleteMutation.Field()
     delete_slots = SlotBatchDeleteMutation.Field()
     update_slots = SlotBatchPatchMutation.Field()
 
-    batch_create_timebound_course_config = TimeboundCourseConfigBatchCreateMutation.Field()
-    create_timebound_course_config = TimeboundCourseConfigCreateMutation.Field()
-    delete_timebound_course_config = TimeboundCourseConfigDeleteMutation.Field()
+    create_timebound_course_configs = TimeboundCourseConfigBatchCreateMutation.Field()
+    delete_timebound_course_configs = TimeboundCourseConfigBatchDeleteMutation.Field()
     update_timebound_course_configs = TimeboundCourseConfigBatchPatchMutation.Field()
     carry_over_slots = CarryOverSlotsMutation.Field()
     copy_slots_from_grid = CopySlotsFromDifferentTimeGridMutation.Field()
 
-    create_validity_range = ValidityRangeCreateMutation.Field()
-    delete_validity_range = ValidityRangeDeleteMutation.Field()
+    create_validity_ranges = ValidityRangeBatchCreateMutation.Field()
     delete_validity_ranges = ValidityRangeBatchDeleteMutation.Field()
     update_validity_ranges = ValidityRangeBatchPatchMutation.Field()
 
-    create_time_grid = TimeGridCreateMutation.Field()
-    delete_time_grid = TimeGridDeleteMutation.Field()
+    create_time_grids = TimeGridBatchCreateMutation.Field()
     delete_time_grids = TimeGridBatchDeleteMutation.Field()
     update_time_grids = TimeGridBatchDeleteMutation.Field()
 
-    create_lesson = LessonCreateMutation.Field()
-    delete_lesson = LessonDeleteMutation.Field()
+    create_lessons = LessonBatchCreateMutation.Field()
     delete_lessons = LessonBatchDeleteMutation.Field()
-    update_lesson = LessonPatchMutation.Field()
     update_lessons = LessonBatchPatchMutation.Field()
 
-    create_supervision = SupervisionCreateMutation.Field()
-    delete_supervision = SupervisionDeleteMutation.Field()
+    create_supervisions = SupervisionBatchCreateMutation.Field()
     delete_supervisions = SupervisionBatchDeleteMutation.Field()
     update_supervisions = SupervisionBatchPatchMutation.Field()
diff --git a/aleksis/apps/lesrooster/schema/break_slot.py b/aleksis/apps/lesrooster/schema/break_slot.py
index fbc54e417fdd85dc7e626fa5cca51a0e207446d2..ed6aa19e5c51f06e2074e3de6eae7c1ffebb2ded 100644
--- a/aleksis/apps/lesrooster/schema/break_slot.py
+++ b/aleksis/apps/lesrooster/schema/break_slot.py
@@ -4,12 +4,10 @@ from graphene_django_cud.mutations import (
     DjangoBatchCreateMutation,
     DjangoBatchDeleteMutation,
     DjangoBatchPatchMutation,
-    DjangoCreateMutation,
 )
 from guardian.shortcuts import get_objects_for_user
 
 from aleksis.core.schema.base import (
-    DeleteMutation,
     DjangoFilterMixin,
     PermissionBatchDeleteMixin,
     PermissionBatchPatchMixin,
@@ -45,28 +43,6 @@ class BreakSlotType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
         return queryset
 
 
-class BreakSlotCreateMutation(DjangoCreateMutation):
-    class Meta:
-        model = BreakSlot
-        return_field_name = "breakSlot"
-        field_types = {"weekday": graphene.Int()}
-        only_fields = (
-            "id",
-            "time_grid",
-            "name",
-            "weekday",
-            "period",
-            "time_start",
-            "time_end",
-        )
-        permissions = ("lesrooster.create_breakslot_rule",)
-
-
-class BreakSlotDeleteMutation(DeleteMutation):
-    klass = BreakSlot
-    permission_required = "lesrooster.delete_breakslot_rule"
-
-
 class BreakSlotBatchCreateMutation(PermissionBatchPatchMixin, DjangoBatchCreateMutation):
     class Meta:
         model = BreakSlot
diff --git a/aleksis/apps/lesrooster/schema/lesson.py b/aleksis/apps/lesrooster/schema/lesson.py
index de013bc0bc4dcccd286fa3011e5aec775ee025df..a3ceb68e72cac1bdeba2940a245497373c481574 100644
--- a/aleksis/apps/lesrooster/schema/lesson.py
+++ b/aleksis/apps/lesrooster/schema/lesson.py
@@ -1,21 +1,18 @@
 import graphene
 from graphene_django.types import DjangoObjectType
 from graphene_django_cud.mutations import (
+    DjangoBatchCreateMutation,
     DjangoBatchDeleteMutation,
     DjangoBatchPatchMutation,
-    DjangoCreateMutation,
-    DjangoPatchMutation,
 )
 from guardian.shortcuts import get_objects_for_user
 from recurrence import Recurrence, deserialize, serialize
 
 from aleksis.core.schema.base import (
-    DeleteMutation,
     DjangoFilterMixin,
     OptimisticResponseTypeMixin,
     PermissionBatchDeleteMixin,
     PermissionBatchPatchMixin,
-    PermissionPatchMixin,
     PermissionsTypeMixin,
 )
 
@@ -47,7 +44,7 @@ class LessonType(
         return serialize(root.recurrence)
 
 
-class LessonCreateMutation(DjangoCreateMutation):
+class LessonBatchCreateMutation(DjangoBatchCreateMutation):
     class Meta:
         model = Lesson
         only_fields = (
@@ -68,11 +65,6 @@ class LessonCreateMutation(DjangoCreateMutation):
         return deserialize(value)
 
 
-class LessonDeleteMutation(DeleteMutation):
-    klass = Lesson
-    permission_required = "lesrooster.delete_lesson_rule"
-
-
 class LessonBatchDeleteMutation(PermissionBatchDeleteMixin, DjangoBatchDeleteMutation):
     class Meta:
         model = Lesson
@@ -98,24 +90,3 @@ class LessonBatchPatchMutation(PermissionBatchPatchMixin, DjangoBatchPatchMutati
     @classmethod
     def handle_recurrence(cls, value: str, name, info) -> Recurrence:
         return deserialize(value)
-
-
-class LessonPatchMutation(PermissionPatchMixin, DjangoPatchMutation):
-    class Meta:
-        model = Lesson
-        only_fields = (
-            "id",
-            "course",
-            "slot_start",
-            "slot_end",
-            "rooms",
-            "teachers",
-            "subject",
-            "recurrence",
-        )
-        field_types = {"recurrence": graphene.String()}
-        permissions = ("lesrooster.edit_lesson_rule",)
-
-    @classmethod
-    def handle_recurrence(cls, value: str, name, info) -> Recurrence:
-        return deserialize(value)
diff --git a/aleksis/apps/lesrooster/schema/slot.py b/aleksis/apps/lesrooster/schema/slot.py
index 6c2de6a501613bd058dd72a141912d644ee01b87..73f135a9b223108d1ed034b786d727c9e9750877 100644
--- a/aleksis/apps/lesrooster/schema/slot.py
+++ b/aleksis/apps/lesrooster/schema/slot.py
@@ -6,12 +6,10 @@ from graphene_django_cud.mutations import (
     DjangoBatchCreateMutation,
     DjangoBatchDeleteMutation,
     DjangoBatchPatchMutation,
-    DjangoCreateMutation,
 )
 from guardian.shortcuts import get_objects_for_user
 
 from aleksis.core.schema.base import (
-    DeleteMutation,
     DjangoFilterMixin,
     PermissionBatchDeleteMixin,
     PermissionBatchPatchMixin,
@@ -50,19 +48,6 @@ class SlotType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
         return root.get_real_instance_class().__name__
 
 
-class SlotCreateMutation(DjangoCreateMutation):
-    class Meta:
-        model = Slot
-        field_types = {"weekday": graphene.Int()}
-        only_fields = ("id", "time_grid", "name", "weekday", "period", "time_start", "time_end")
-        permissions = ("lesrooster.create_slot_rule",)
-
-
-class SlotDeleteMutation(DeleteMutation):
-    klass = Slot
-    permission_required = "lesrooster.delete_slot"
-
-
 class SlotBatchCreateMutation(PermissionBatchPatchMixin, DjangoBatchCreateMutation):
     class Meta:
         model = Slot
diff --git a/aleksis/apps/lesrooster/schema/supervision.py b/aleksis/apps/lesrooster/schema/supervision.py
index e028cf8662ccc758f2c4bbbe2574f0bf8a0bc2a7..80603a12ed15cdf8237bfb7f560c338ec14f152e 100644
--- a/aleksis/apps/lesrooster/schema/supervision.py
+++ b/aleksis/apps/lesrooster/schema/supervision.py
@@ -1,15 +1,14 @@
 import graphene
 from graphene_django.types import DjangoObjectType
 from graphene_django_cud.mutations import (
+    DjangoBatchCreateMutation,
     DjangoBatchDeleteMutation,
     DjangoBatchPatchMutation,
-    DjangoCreateMutation,
 )
 from guardian.shortcuts import get_objects_for_user
 from recurrence import Recurrence, deserialize, serialize
 
 from aleksis.core.schema.base import (
-    DeleteMutation,
     DjangoFilterMixin,
     PermissionBatchDeleteMixin,
     PermissionBatchPatchMixin,
@@ -53,7 +52,7 @@ class SupervisionType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType)
         return serialize(root.recurrence)
 
 
-class SupervisionCreateMutation(DjangoCreateMutation):
+class SupervisionBatchCreateMutation(DjangoBatchCreateMutation):
     class Meta:
         model = Supervision
         field_types = {"recurrence": graphene.String()}
@@ -72,11 +71,6 @@ class SupervisionCreateMutation(DjangoCreateMutation):
         return deserialize(value)
 
 
-class SupervisionDeleteMutation(DeleteMutation):
-    klass = Supervision
-    permission_required = "lesrooster.delete_supervision_rule"
-
-
 class SupervisionBatchDeleteMutation(PermissionBatchDeleteMixin, DjangoBatchDeleteMutation):
     class Meta:
         model = Supervision
diff --git a/aleksis/apps/lesrooster/schema/time_grid.py b/aleksis/apps/lesrooster/schema/time_grid.py
index 906b16ce6d78ab81a991b783506d6d5777569ece..a823920d1fe3cda1e19eaf6d6facb8cdc3695363 100644
--- a/aleksis/apps/lesrooster/schema/time_grid.py
+++ b/aleksis/apps/lesrooster/schema/time_grid.py
@@ -1,13 +1,12 @@
 from graphene_django.types import DjangoObjectType
 from graphene_django_cud.mutations import (
+    DjangoBatchCreateMutation,
     DjangoBatchDeleteMutation,
     DjangoBatchPatchMutation,
-    DjangoCreateMutation,
 )
 from guardian.shortcuts import get_objects_for_user
 
 from aleksis.core.schema.base import (
-    DeleteMutation,
     DjangoFilterMixin,
     PermissionBatchDeleteMixin,
     PermissionBatchPatchMixin,
@@ -40,7 +39,7 @@ class TimeGridType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType):
         return queryset
 
 
-class TimeGridCreateMutation(DjangoCreateMutation):
+class TimeGridBatchCreateMutation(DjangoBatchCreateMutation):
     class Meta:
         model = TimeGrid
         permissions = ("lesrooster.create_timegrid_rule",)
@@ -51,11 +50,6 @@ class TimeGridCreateMutation(DjangoCreateMutation):
         )
 
 
-class TimeGridDeleteMutation(DeleteMutation):
-    klass = TimeGrid
-    permission_required = "lesrooster.delete_timegrid_rule"
-
-
 class TimeGridBatchDeleteMutation(PermissionBatchDeleteMixin, DjangoBatchDeleteMutation):
     class Meta:
         model = TimeGrid
diff --git a/aleksis/apps/lesrooster/schema/timebound_course_config.py b/aleksis/apps/lesrooster/schema/timebound_course_config.py
index 978260ba70e07acfcfeeac6d1223601f7a07d363..b0c1feaaaf727c8393c62352ea1415ca78245748 100644
--- a/aleksis/apps/lesrooster/schema/timebound_course_config.py
+++ b/aleksis/apps/lesrooster/schema/timebound_course_config.py
@@ -2,15 +2,14 @@ import graphene
 from graphene_django.types import DjangoObjectType
 from graphene_django_cud.mutations import (
     DjangoBatchCreateMutation,
+    DjangoBatchDeleteMutation,
     DjangoBatchPatchMutation,
-    DjangoCreateMutation,
 )
 from guardian.shortcuts import get_objects_for_user
 
 from aleksis.apps.cursus.models import Course, Subject
 from aleksis.apps.cursus.schema import CourseInterface, CourseType, SubjectType
 from aleksis.core.schema.base import (
-    DeleteMutation,
     DjangoFilterMixin,
     PermissionBatchPatchMixin,
     PermissionsTypeMixin,
@@ -71,12 +70,13 @@ class LesroosterExtendedCourseType(CourseType):
 
     @staticmethod
     def resolve_lr_timebound_course_configs(root, info, **kwargs):
-        if not info.context.user.has_perm("lesrostter.view_timeboundcourseconfig_rule"):
+        if not info.context.user.has_perm("lesrooster.view_timeboundcourseconfig_rule"):
             return get_objects_for_user(
                 info.context.user,
                 "lesrooster.view_timeboundcourseconfig",
                 root.lr_timebound_course_configs.all(),
             )
+        return root.lr_timebound_course_configs.all()
 
 
 class LesroosterExtendedSubjectType(SubjectType):
@@ -86,23 +86,17 @@ class LesroosterExtendedSubjectType(SubjectType):
     courses = graphene.List(LesroosterExtendedCourseType)
 
 
-class TimeboundCourseConfigCreateMutation(DjangoCreateMutation):
+class TimeboundCourseConfigBatchCreateMutation(DjangoBatchCreateMutation):
     class Meta:
         model = TimeboundCourseConfig
         fields = ("id", "course", "validity_range", "lesson_quota", "teachers")
         permissions = ("lesrooster.create_timeboundcourseconfig_rule",)
 
 
-class TimeboundCourseConfigBatchCreateMutation(DjangoBatchCreateMutation):
+class TimeboundCourseConfigBatchDeleteMutation(DjangoBatchDeleteMutation):
     class Meta:
         model = TimeboundCourseConfig
-        fields = ("id", "course", "validity_range", "lesson_quota", "teachers")
-        permissions = ("lesrooster.create_timeboundcourseconfig_rule",)
-
-
-class TimeboundCourseConfigDeleteMutation(DeleteMutation):
-    klass = TimeboundCourseConfig
-    permission_required = "lesrooster.delete_timeboundcourseconfig_rule"
+        permission_required = "lesrooster.delete_timeboundcourseconfig_rule"
 
 
 class TimeboundCourseConfigBatchPatchMutation(PermissionBatchPatchMixin, DjangoBatchPatchMutation):
diff --git a/aleksis/apps/lesrooster/schema/validity_range.py b/aleksis/apps/lesrooster/schema/validity_range.py
index 040fc2778846be7d0e32354bd534fef0034b8880..a323858535e6fb3bc355bc3f4ca42b6f7a55aaed 100644
--- a/aleksis/apps/lesrooster/schema/validity_range.py
+++ b/aleksis/apps/lesrooster/schema/validity_range.py
@@ -1,14 +1,13 @@
 import graphene
 from graphene_django.types import DjangoObjectType
 from graphene_django_cud.mutations import (
+    DjangoBatchCreateMutation,
     DjangoBatchDeleteMutation,
     DjangoBatchPatchMutation,
-    DjangoCreateMutation,
 )
 from guardian.shortcuts import get_objects_for_user
 
 from aleksis.core.schema.base import (
-    DeleteMutation,
     DjangoFilterMixin,
     PermissionBatchDeleteMixin,
     PermissionBatchPatchMixin,
@@ -40,7 +39,7 @@ class ValidityRangeType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectTyp
         return queryset
 
 
-class ValidityRangeCreateMutation(DjangoCreateMutation):
+class ValidityRangeBatchCreateMutation(DjangoBatchCreateMutation):
     class Meta:
         model = ValidityRange
         permissions = ("lesrooster.create_validity_range_rule",)
@@ -56,11 +55,6 @@ class ValidityRangeCreateMutation(DjangoCreateMutation):
         field_types = {"status": graphene.String()}
 
 
-class ValidityRangeDeleteMutation(DeleteMutation):
-    klass = ValidityRange
-    permission_required = "lesrooster.delete_validityrange_rule"
-
-
 class ValidityRangeBatchDeleteMutation(PermissionBatchDeleteMixin, DjangoBatchDeleteMutation):
     class Meta:
         model = ValidityRange
diff --git a/pyproject.toml b/pyproject.toml
index 5266c94c7c4dab90cfadb10d54e7ee7aa4ea70fc..b451c0d2f9d14f85f9ef2f7d42f5fb82be2e7783 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "AlekSIS-App-Lesrooster"
-version = "0.1"
+version = "0.1.0.dev0"
 packages = [
     { include = "aleksis" }
 ]
@@ -32,9 +32,10 @@ priority = "primary"
 name = "gitlab"
 url = "https://edugit.org/api/v4/projects/461/packages/pypi/simple"
 priority = "supplemental"
+
 [tool.poetry.dependencies]
 python = "^3.10"
-AlekSIS-Core = "^4.0.0.dev2"
+AlekSIS-Core = "^4.0.0.dev4"
 AlekSIS-App-Chronos = "^4.0.0.dev1"
 AlekSIS-App-Cursus = "^0.1.dev0"
 django-recurrence = "^1.11.1"