diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue index 02349f01add8910e22f6cdfdfb16d99ad303ffb5..0fb5b638c873bd3f20b16ea62ab438693c9f6c8a 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue +++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue @@ -3,16 +3,22 @@ import AbsenceReasonButtons from "aleksis.apps.kolego/components/AbsenceReasonBu import AbsenceReasonChip from "aleksis.apps.kolego/components/AbsenceReasonChip.vue"; import AbsenceReasonGroupSelect from "aleksis.apps.kolego/components/AbsenceReasonGroupSelect.vue"; import DialogCloseButton from "aleksis.core/components/generic/buttons/DialogCloseButton.vue"; +import SecondaryActionButton from "aleksis.core/components/generic/buttons/SecondaryActionButton.vue"; import MobileFullscreenDialog from "aleksis.core/components/generic/dialogs/MobileFullscreenDialog.vue"; import updateParticipationMixin from "./updateParticipationMixin.js"; import deepSearchMixin from "aleksis.core/mixins/deepSearchMixin.js"; import LessonInformation from "../documentation/LessonInformation.vue"; +import { + extendParticipationStatuses, + updateParticipationStatuses, +} from "./participationStatus.graphql"; import SlideIterator from "aleksis.core/components/generic/SlideIterator.vue"; import PersonalNotes from "../personal_notes/PersonalNotes.vue"; import ExtraMarkChip from "../../extra_marks/ExtraMarkChip.vue"; import TardinessChip from "./TardinessChip.vue"; import TardinessField from "./TardinessField.vue"; import ExtraMarkButtons from "../../extra_marks/ExtraMarkButtons.vue"; +import MessageBox from "aleksis.core/components/generic/MessageBox.vue"; export default { name: "ManageStudentsDialog", @@ -26,7 +32,9 @@ export default { AbsenceReasonButtons, PersonalNotes, LessonInformation, + MessageBox, MobileFullscreenDialog, + SecondaryActionButton, SlideIterator, TardinessField, DialogCloseButton, @@ -39,6 +47,14 @@ export default { loadSelected: false, selected: [], isExpanded: false, + markAsAbsentDay: { + showAlert: false, + num: 0, + reason: "no reason", + name: "nobody", + participationIDs: [], + loading: false, + }, }; }, props: { @@ -69,6 +85,71 @@ export default { this.$set(this.selected, []); this.$refs.iterator.selected = []; }, + activateFullDayDialog(items) { + const itemIds = items.map((item) => item.id); + const participations = this.documentation.participations.filter((part) => + itemIds.includes(part.id), + ); + + if (this.markAsAbsentDay.num === 1) { + this.markAsAbsentDay.name = participations[0].person.firstName; + } + + this.$set(this.markAsAbsentDay, "participationIDs", itemIds); + + this.markAsAbsentDay.loading = false; + this.markAsAbsentDay.showAlert = true; + }, + beforeSendToServer() { + this.markAsAbsentDay.showAlert = false; + this.markAsAbsentDay.participationIDs = []; + }, + duringUpdateSendToServer( + _participations, + _field, + _value, + incomingStatuses, + ) { + this.markAsAbsentDay.reason = incomingStatuses[0].absenceReason?.name; + this.markAsAbsentDay.num = incomingStatuses.length; + }, + afterSendToServer(_participations, field, value) { + if (field === "absenceReason" && value !== "present") { + this.$once("save", this.activateFullDayDialog); + } + }, + markAsAbsentDayClick() { + this.markAsAbsentDay.loading = true; + + this.mutate( + extendParticipationStatuses, + { + input: this.markAsAbsentDay.participationIDs, + }, + (storedDocumentations, incomingStatuses) => { + const documentation = storedDocumentations.find( + (doc) => doc.id === this.documentation.id, + ); + + incomingStatuses.forEach((newStatus) => { + const participationStatus = documentation.participations.find( + (part) => part.id === newStatus.id, + ); + participationStatus.baseAbsence = newStatus.baseAbsence; + participationStatus.isOptimistic = newStatus.isOptimistic; + }); + + this.markAsAbsentDay.reason = "no reason"; + this.markAsAbsentDay.num = 0; + this.markAsAbsentDay.participationIDs = []; + this.markAsAbsentDay.loading = false; + + this.markAsAbsentDay.showAlert = false; + + return storedDocumentations; + }, + ); + }, }, }; </script> @@ -106,6 +187,40 @@ export default { class="pt-4 full-width" /> </v-scroll-x-transition> + <message-box + v-model="markAsAbsentDay.showAlert" + color="success" + icon="$success" + transition="slide-y-transition" + dismissible + class="mt-4 mb-0 full-width" + > + <div class="text-subtitle-2"> + {{ + $tc( + "alsijil.coursebook.mark_as_absent_day.title", + markAsAbsentDay.num, + markAsAbsentDay, + ) + }} + </div> + <p class="text-body-2 pa-0 ma-0" style="word-break: break-word"> + {{ + $t( + "alsijil.coursebook.mark_as_absent_day.description", + markAsAbsentDay, + ) + }} + </p> + + <secondary-action-button + color="success" + i18n-key="alsijil.coursebook.mark_as_absent_day.action_button" + class="mt-2" + :loading="markAsAbsentDay.loading" + @click="markAsAbsentDayClick" + /> + </message-box> </template> <template #content> <slide-iterator diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql index ce296d2b684c1f45928e1d94e8e584f7b845bb69..39eb59528c4f876eb2c75c20f187e5ded6fae2bc 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql +++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql @@ -64,3 +64,14 @@ mutation touchDocumentation($documentationId: ID!) { } } } + +mutation extendParticipationStatuses($input: [ID]!) { + extendParticipationStatuses(input: $input) { + items: participations { + id + } + absences { + id + } + } +} diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/sendToServerMixin.js b/aleksis/apps/alsijil/frontend/components/coursebook/absences/sendToServerMixin.js index 55cae9e6e8d69b0f4e5d299e9869228ccf9c2702..e269bddee80dbe9056e399d63581212ebfdd36c4 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/sendToServerMixin.js +++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/sendToServerMixin.js @@ -28,6 +28,8 @@ export default { return; } + this.beforeSendToServer(participations, field, value); + this.mutate( updateParticipationStatuses, { @@ -51,9 +53,18 @@ export default { participationStatus.isOptimistic = newStatus.isOptimistic; }); + this.duringUpdateSendToServer( + participations, + field, + value, + incomingStatuses, + ); + return storedDocumentations; }, ); + + this.afterSendToServer(participations, field, value); }, addExtraMarks(participations, extraMarkId) { // Get all participation statuses without this extra mark and get the respective person ids @@ -91,5 +102,14 @@ export default { }, ); }, + beforeSendToServer(_participations, _field, _value) { + // Noop hook + }, + duringUpdateSendToServer(_participations, _field, _value, _incoming) { + // Noop hook + }, + afterSendToServer(_participations, _field, _value) { + // Noop hook + }, }, }; diff --git a/aleksis/apps/alsijil/frontend/messages/en.json b/aleksis/apps/alsijil/frontend/messages/en.json index d1a039d4caa7b94b15fdeb36a396b6ec745eef70..0d39f282a34715bfc115554003999f943b9ca547 100644 --- a/aleksis/apps/alsijil/frontend/messages/en.json +++ b/aleksis/apps/alsijil/frontend/messages/en.json @@ -111,6 +111,11 @@ "lessons": "No lessons | 1 lesson | {count} lessons", "success": "The absences were registered successfully.", "warning": "The following lessons are in the selected time period. Please check that you want to register the absences for these lessons before confirming." + }, + "mark_as_absent_day": { + "title": "Error: no person | Successfully marked {name} as {reason} | Successfully marked {n} people as {reason}", + "description": "Do you want to mark them as {reason} for the rest of their day?", + "action_button": "Extend absence" } }, "personal_notes": { diff --git a/aleksis/apps/alsijil/schema/__init__.py b/aleksis/apps/alsijil/schema/__init__.py index ab87c4306cd144d4c5f27c00914aceeb865584a7..97ec4f187e036a246772b709c75942018fb1d4d9 100644 --- a/aleksis/apps/alsijil/schema/__init__.py +++ b/aleksis/apps/alsijil/schema/__init__.py @@ -32,7 +32,10 @@ from .extra_marks import ( ExtraMarkBatchPatchMutation, ExtraMarkType, ) -from .participation_status import ParticipationStatusBatchPatchMutation +from .participation_status import ( + ExtendParticipationStatusToAbsenceBatchMutation, + ParticipationStatusBatchPatchMutation, +) from .personal_note import ( PersonalNoteBatchCreateMutation, PersonalNoteBatchDeleteMutation, @@ -233,6 +236,7 @@ class Mutation(graphene.ObjectType): touch_documentation = TouchDocumentationMutation.Field() update_participation_statuses = ParticipationStatusBatchPatchMutation.Field() create_absences_for_persons = AbsencesForPersonsCreateMutation.Field() + extend_participation_statuses = ExtendParticipationStatusToAbsenceBatchMutation.Field() create_extra_marks = ExtraMarkBatchCreateMutation.Field() update_extra_marks = ExtraMarkBatchPatchMutation.Field() diff --git a/aleksis/apps/alsijil/schema/participation_status.py b/aleksis/apps/alsijil/schema/participation_status.py index 951ab4419341641cc26059a18f04f91890749f1c..2b9ebb25f37fabcc58415da613980d171114996d 100644 --- a/aleksis/apps/alsijil/schema/participation_status.py +++ b/aleksis/apps/alsijil/schema/participation_status.py @@ -1,10 +1,16 @@ +import datetime + from django.core.exceptions import PermissionDenied +from django.utils.translation import gettext_lazy as _ import graphene from graphene_django import DjangoObjectType +from reversion import create_revision, set_comment, set_user from aleksis.apps.alsijil.models import NewPersonalNote, ParticipationStatus from aleksis.apps.alsijil.schema.personal_note import PersonalNoteType +from aleksis.apps.kolego.models import Absence +from aleksis.apps.kolego.schema.absence import AbsenceType from aleksis.core.schema.base import ( BaseBatchPatchMutation, DjangoFilterMixin, @@ -70,3 +76,77 @@ class ParticipationStatusBatchPatchMutation(BaseBatchPatchMutation): "alsijil.edit_participation_status_for_documentation_rule", obj.related_documentation ): raise PermissionDenied() + + +class ExtendParticipationStatusToAbsenceBatchMutation(graphene.Mutation): + class Arguments: + input = graphene.List(graphene.ID, description=_("List of ParticipationStatus IDs")) + + participations = graphene.List(ParticipationStatusType) + absences = graphene.List(AbsenceType) + + @classmethod + def create_absence(cls, info, participation_id): + participation = ParticipationStatus.objects.get(pk=participation_id) + + if participation.date_end: + end_date = participation.date_end + else: + end_date = ParticipationStatus.value_end_datetime(participation).date() + + end_datetime = datetime.datetime.combine( + end_date, datetime.time.max, participation.timezone + ) + + if participation.base_absence: + # Update the base absence to increase length if needed + absence = participation.base_absence + + if absence.date_end: + if absence.date_end < end_date: + absence.date_end = end_date + absence.save() + + return participation, absence + + # Absence uses a datetime + if absence.datetime_end.astimezone(absence.timezone) < end_datetime: + # The end date ends after the previous absence end + absence.datetime_end = end_datetime + absence.save() + + return participation, absence + + else: + # No base absence, simply create one + data = dict( + reason_id=participation.absence_reason.id, + person=participation.person, + ) + + if participation.date_start: + data["date_start"] = participation.date_start + data["date_end"] = end_date + else: + data["datetime_start"] = ParticipationStatus.value_start_datetime(participation) + data["datetime_end"] = end_datetime + + absence, __ = Absence.objects.get_or_create(**data) + + participation.base_absence = absence + participation.save() + + return participation, absence + + @classmethod + def mutate(cls, root, info, input): # noqa + with create_revision(): + set_user(info.context.user) + set_comment(_("Extended absence reason from coursebook.")) + participations, absences = zip( + *[cls.create_absence(info, participation_id) for participation_id in input] + ) + + return ExtendParticipationStatusToAbsenceBatchMutation( + participations=participations, absences=absences + )