Skip to content
Snippets Groups Projects
SubstitutionCard.vue 12.47 KiB
<script setup>
import SubstitutionInformation from "./SubstitutionInformation.vue";
import TeacherField from "aleksis.apps.cursus/components/TeacherField.vue";
import SubjectField from "aleksis.apps.cursus/components/SubjectField.vue";

import { gqlRooms } from "../amendLesson.graphql";

import createOrPatchMixin from "aleksis.core/mixins/createOrPatchMixin.js";
import deleteMixin from "aleksis.core/mixins/deleteMixin.js";
</script>

<template>
  <v-card class="my-1 full-width" :loading="loading">
    <!-- flex-md-row zeile ab medium -->
    <!-- align-stretch - stretch full-width -->
    <v-card-text
      class="full-width main-body pa-2"
      :class="{
        vertical: $vuetify.breakpoint.mobile,
      }"
    >
      <substitution-information :substitution="substitution" />

      <v-spacer />

      <subject-field
        :enable-create="false"
        dense
        outlined
        hide-details
        :value="subject"
        :disabled="loading"
        :label="$t('chronos.substitutions.overview.subject.label')"
        @input="subjectInput"
      />

      <v-autocomplete
        :value="substitutionRoomIDs"
        multiple
        chips
        deletable-chips
        dense
        hide-details
        outlined
        :items="amendableRooms"
        item-text="name"
        item-value="id"
        :disabled="loading"
        :label="$t('chronos.substitutions.overview.rooms.label')"
        @input="roomsInput"
      >
        <template #prepend-inner>
          <v-chip
            v-if="roomsWithStatus.filter((t) => t.status === 'regular').length"
            v-for="room in roomsWithStatus.filter(
              (t) => t.status === 'regular',
            )"
            class="mb-1"
            small
          >
            {{ room.shortName }}
          </v-chip>
          <v-chip
            v-if="roomsWithStatus.filter((t) => t.status === 'removed').length"
            v-for="room in roomsWithStatus.filter(
              (t) => t.status === 'removed',
            )"
            outlined
            color="error"
            class="mb-1"
            small
          >
            <v-icon left small>mdi-cancel</v-icon>
            <div class="text-decoration-line-through">
              {{ room.shortName ? room.shortName : room.name }}
            </div>
          </v-chip>
        </template>
        <template #selection="data">
          <v-chip
            v-bind="data.attrs"
            :input-value="data.selected"
            v-if="getRoomStatus(data.item) === 'new'"
            close
            class="mb-1 mt-1"
            small
            outlined
            color="success"
            @click:close="removeRoom(data.item)"
          >
            <v-icon left small> mdi-plus </v-icon>
            {{ data.item.shortName ? data.item.shortName : data.item.name }}
          </v-chip>
        </template>
      </v-autocomplete>

      <teacher-field
        :priority-subject="subject"
        :show-subjects="true"
        :availability-datetime-start="substitution.datetimeStart"
        :availability-datetime-end="substitution.datetimeEnd"
        :value="substitutionTeacherIDs"
        chips
        deletable-chips
        dense
        hide-details
        outlined
        :disabled="loading"
        :label="$t('chronos.substitutions.overview.teacher.label')"
        @input="teachersInput"
      >
        <template #prepend-inner>
          <v-tooltip bottom>
            <template #activator="{ on, attrs }">
              <v-chip
                v-for="teacher in teachersWithStatus.filter(
                  (t) => t.status === 'removed',
                )"
                outlined
                color="error"
                class="mb-1"
                small
                v-on="on"
                v-bind="attrs"
              >
                <v-icon left small>mdi-account-off-outline</v-icon>
                <div class="text-decoration-line-through">
                  {{ teacher.fullName }}
                </div>
              </v-chip>
            </template>
            <span>{{
              $t("chronos.substitutions.overview.teacher.status.absent")
            }}</span>
          </v-tooltip>
        </template>
        <template #selection="data">
          <v-tooltip bottom>
            <template #activator="{ on, attrs }">
              <v-chip
                v-bind="{ ...data.attrs, ...attrs }"
                :input-value="data.selected"
                close
                class="mb-1 mt-1"
                small
                :outlined="getTeacherStatus(data.item) === 'new'"
                :color="getTeacherStatus(data.item) === 'new' ? 'success' : ''"
                @click:close="removeTeacher(data.item)"
                v-on="on"
              >
                <v-icon left small v-if="getTeacherStatus(data.item) === 'new'">
                  mdi-account-plus-outline
                </v-icon>
                {{ data.item.fullName }}
              </v-chip>
            </template>
            <span>{{
              getTeacherStatus(data.item) === "new"
                ? $t("chronos.substitutions.overview.teacher.status.new")
                : $t("chronos.substitutions.overview.teacher.status.regular")
            }}</span>
          </v-tooltip>
        </template>
      </teacher-field>

      <v-text-field
        dense
        outlined
        hide-details
        :label="$t('chronos.substitutions.overview.comment')"
        :value="substitution.comment"
        @input="comment = $event"
        @focusout="save"
        @keydown.enter="save"
      />

      <v-btn-toggle
        mandatory
        dense
        :color="substitution.cancelled ? 'error' : 'success'"
        :disabled="loading"
        class="justify-self-end"
        :value="substitution.cancelled"
        @change="save"
      >
        <v-btn
          outlined
          :value="false"
          @click="cancelled = false"
        >
          {{ $t("chronos.substitutions.overview.cancel.not_cancelled") }}
        </v-btn>
        <v-btn
          outlined
          :value="true"
          @click="cancelled = true"
        >
          {{ $t("chronos.substitutions.overview.cancel.cancelled") }}
        </v-btn>
      </v-btn-toggle>
    </v-card-text>
    <v-divider />
  </v-card>
</template>

<script>
export default {
  name: "SubstitutionCard",
  emits: ["open", "close", "delete"],
  mixins: [createOrPatchMixin, deleteMixin],
  data() {
    return {
      loading: false,
      teachers: [],
      rooms: [],
      substitutionSubject: null,
      comment: null,
      cancelled: null,
    };
  },
  props: {
    substitution: {
      type: Object,
      required: true,
    },
  },
  methods: {
    handleUpdateAfterCreateOrPatch(itemId) {
      return (cached, incoming) => {
        for (const object of incoming) {
          console.log("summary: handleUpdateAfterCreateOrPatch", object);
          // Replace the current substitution
          const index = cached.findIndex(
            (o) => o[itemId] === this.substitution.id,
          );
          // merged with the incoming partial substitution
          // if creation of proper substitution from dummy one, set ID of substitution currently being edited as oldID so that key in overview doesn't change
          cached[index] = {
            ...this.substitution,
            ...object,
            oldId:
              this.substitution.id !== object.id
                ? this.substitution.id
                : this.substitution.oldId,
          };
        }
        return cached;
      };
    },
    handleUpdateAfterDelete(ids, itemId) {
      return (cached, incoming) => {
        for (const id of ids) {
          // Remove item from cached data or reset it to old ID, if present
          const index = cached.findIndex((o) => o[itemId] === id);
          if (cached[index].oldId) {
            cached[index].id = cached[index].oldId;
            cached[index].oldId = null;
          } else {
            this.$emit("delete", cached[index].datetimeStart);
          }
        }
        return cached;
      };
    },
    getTeacherStatus(teacher) {
      return this.teachersWithStatus.find((t) => t.id === teacher.id)?.status;
    },
    getRoomStatus(room) {
      return this.roomsWithStatus.find((r) => r.id === room.id)?.status;
    },
    removeTeacher(teacher) {
      this.teachers = this.substitutionTeacherIDs.filter((t) => t !== teacher.id);
      this.save(true);
    },
    removeRoom(room) {
      this.rooms = this.substitutionRoomIDs.filter((r) => r !== room.id);
      this.save(true);
    },
    subjectInput(subject) {
      this.substitutionSubject = subject.id;
      this.save();
    },
    teachersInput(teachers) {
      this.teachers = teachers;
      this.save();
    },
    roomsInput(rooms) {
      this.rooms = rooms;
      this.save();
    },
    cancelledInput(cancelled) {
      this.cancelled = cancelled;
      this.save();
    },
    save(allowEmpty = false) {
      if (
        this.teachers.length ||
        this.rooms.length ||
        this.substitutionSubject !== null ||
        (this.comment !== null &&
        this.comment !== "") ||
        this.cancelled !== null
      ) {
        this.createOrPatch([
          {
            id: this.substitution.id,
            ...((allowEmpty || this.teachers.length) && { teachers: this.teachers }),
            ...((allowEmpty || this.rooms.length) && { rooms: this.rooms }),
            ...(this.substitutionSubject !== null && { subject: this.substitutionSubject }),
            ...((this.comment !== null && this.comment !== "") && { comment: this.comment }),
            ...(this.cancelled !== null && { cancelled: this.cancelled }),
          },
        ]);
        this.teachers = [];
        this.rooms = [];
        this.substitutionSubject = null;
        this.comment = null;
        this.cancelled = null;
      } else if (!this.substitution.id.startsWith("DUMMY")) {
        this.delete([this.substitution]);
      }
    },
  },
  computed: {
    substitutionTeacherIDs() {
      return this.substitution.teachers.map((teacher) => teacher.id);
    },
    substitutionRoomIDs() {
      return this.substitution.rooms.map((room) => room.id);
    },
    // Group teachers by their substitution status (regular, new, removed)
    teachersWithStatus() {
      // IDs of teachers of amended lesson
      const oldIds = this.substitution.amends.teachers.map(
        (teacher) => teacher.id,
      );
      // IDs of teachers of new substitution lesson
      const newIds = this.substitution.teachers.map((teacher) => teacher.id);
      const allTeachers = new Set(
        this.substitution.amends.teachers.concat(this.substitution.teachers),
      );
      let teachersWithStatus = Array.from(allTeachers).map((teacher) => {
        let status = "regular";
        if (newIds.includes(teacher.id) && !oldIds.includes(teacher.id)) {
          // Mark teacher as being new if they are only linked to the substitution lesson
          status = "new";
        } else if (
          !newIds.includes(teacher.id) &&
          oldIds.includes(teacher.id)
        ) {
          // Mark teacher as being rremoved if they are only linked to the amended lesson
          status = "removed";
        }
        return { ...teacher, status: status };
      });
      return teachersWithStatus;
    },
    roomsWithStatus() {
      const oldIds = this.substitution.amends.rooms.map((room) => room.id);
      const newIds = this.substitution.rooms.map((room) => room.id);
      const allRooms = new Set(
        this.substitution.amends.rooms.concat(this.substitution.rooms),
      );
      let roomsWithStatus = Array.from(allRooms).map((room) => {
        let status = "regular";
        if (newIds.includes(room.id) && !oldIds.includes(room.id)) {
          status = "new";
        } else if (
          newIds.length &&
          !newIds.includes(room.id) &&
          oldIds.includes(room.id)
        ) {
          status = "removed";
        }
        return { ...room, status: status };
      });
      return roomsWithStatus;
    },
    subject() {
      if (this.substitution.subject) {
        return this.substitution.subject;
      }
      else if (this.substitution.course?.subject) {
        return this.substitution.course.subject;
      }
      else if (this.substitution.amends?.subject) {
        return this.substitution.amends.subject;
      }
      return undefined;
    },
  },
  apollo: {
    amendableRooms: gqlRooms,
  },
};
</script>

<style scoped>
.main-body {
  display: grid;
  align-items: center;
  grid-template-columns: 2fr 1fr 1fr 2fr 3fr 1fr 2fr;
  gap: 1em;
}
.vertical {
  grid-template-columns: 1fr;
}
.justify-self-end {
  justify-self: end;
}
</style>