Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • AlekSIS/official/AlekSIS-App-Alsijil
  • sunweaver/AlekSIS-App-Alsijil
  • 8tincsoVluke/AlekSIS-App-Alsijil
  • perfreicpo/AlekSIS-App-Alsijil
  • noifobarep/AlekSIS-App-Alsijil
  • 7ingannisdo/AlekSIS-App-Alsijil
  • unmruntartpa/AlekSIS-App-Alsijil
  • balrorebta/AlekSIS-App-Alsijil
  • comliFdifwa/AlekSIS-App-Alsijil
  • 3ranaadza/AlekSIS-App-Alsijil
10 results
Show changes
Commits on Source (70)
Showing
with 641 additions and 135 deletions
......@@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file.
The format is based on `Keep a Changelog`_,
and this project adheres to `Semantic Versioning`_.
`4.0.0.dev2`_ - 2024-07-10
`4.0.0.dev3`_ - 2024-07-10
--------------------------
Added
......@@ -15,6 +15,11 @@ Added
* Support for entering personal notes for students in the new coursebook interface.
* Support for entering tardiness for students in the new coursebook interface.
`4.0.0.dev2`_ - 2024-07-13
--------------------------
Fixed version of 4.0.0.dev1
`4.0.0.dev1`_ - 2024-06-13
--------------------------
......@@ -149,7 +154,7 @@ Changed
~~~~~~~
* Use start date of current SchoolTerm as default value for PersonalNote filter in overview.
Julia ist eine höhere Programmiersprache, die vor allem für numerisches und wissenschaftliches Rechnen entwickelt wurde und auch als Allzweck-Programmiersprache verwendet werden kann, bei gleichzeitiger Wahrung einer hohen Ausführungsgeschwindigkeit. Wikipedia
Fixed
~~~~~
......@@ -372,3 +377,4 @@ Fixed
.. _4.0.0.dev0: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/4.0.0.dev0
.. _4.0.0.dev1: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/4.0.0.dev1
.. _4.0.0.dev2: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/4.0.0.dev2
.. _4.0.0.dev3: https://edugit.org/AlekSIS/Official/AlekSIS-App-Alsijil/-/tags/4.0.0.dev3
......@@ -53,6 +53,7 @@
:is="itemComponent"
:extra-marks="extraMarks"
:absence-reasons="absenceReasons"
:subjects="subjects"
:documentation="item"
:affected-query="lastQuery"
:value="(selectedParticipations[item.id] ??= [])"
......@@ -87,6 +88,7 @@ import { extraMarks } from "../extra_marks/extra_marks.graphql";
import DocumentationLoader from "./documentation/DocumentationLoader.vue";
import sendToServerMixin from "./absences/sendToServerMixin";
import { absenceReasons } from "./absences/absenceReasons.graphql";
import { subjects } from "aleksis.apps.cursus/components/subject.graphql";
export default {
name: "Coursebook",
......@@ -157,6 +159,7 @@ export default {
hashUpdater: false,
extraMarks: [],
absenceReasons: [],
subjects: [],
selectedParticipations: {},
};
},
......@@ -169,6 +172,10 @@ export default {
query: absenceReasons,
update: (data) => data.items,
},
subjects: {
query: subjects,
update: (data) => data.items,
},
},
computed: {
// Assertion: Should only fire on page load or selection change.
......
......@@ -93,6 +93,7 @@ import SecondaryActionButton from "aleksis.core/components/generic/buttons/Secon
import loadingMixin from "aleksis.core/mixins/loadingMixin.js";
import permissionsMixin from "aleksis.core/mixins/permissions.js";
import mutateMixin from "aleksis.core/mixins/mutateMixin.js";
import { DateTime } from "luxon";
import { createAbsencesForPersons } from "./absenceCreation.graphql";
......@@ -122,6 +123,7 @@ export default {
},
mounted() {
this.addPermissions(["alsijil.view_register_absence_rule"]);
this.clearForm();
},
methods: {
cancel() {
......@@ -131,8 +133,12 @@ export default {
},
clearForm() {
this.persons = [];
this.startDate = "";
this.endDate = "";
this.startDate = DateTime.now()
.startOf("day")
.toISO({ suppressSeconds: true });
this.endDate = DateTime.now()
.endOf("day")
.toISO({ suppressSeconds: true });
this.comment = "";
this.absenceReason = "";
},
......@@ -142,8 +148,8 @@ export default {
createAbsencesForPersons,
{
persons: this.persons.map((p) => p.id),
start: this.startDate,
end: this.endDate,
start: this.$toUTCISO(this.$parseISODate(this.startDate)),
end: this.$toUTCISO(this.$parseISODate(this.endDate)),
comment: this.comment,
reason: this.absenceReason,
},
......
......@@ -26,9 +26,10 @@
<v-row>
<v-col cols="12" :sm="6" class="pl-0">
<div aria-required="true">
<date-field
<date-time-field
:label="$t('forms.labels.start')"
:max="endDate"
:max-date="endDate"
:max-time="maxStartTime"
:rules="$rules().required.build()"
:value="startDate"
@input="$emit('start-date', $event)"
......@@ -37,9 +38,10 @@
</v-col>
<v-col cols="12" :sm="6" class="pr-0">
<div aria-required="true">
<date-field
<date-time-field
:label="$t('forms.labels.end')"
:min="startDate"
:min-date="startDate"
:min-time="minEndTime"
:rules="$rules().required.build()"
:value="endDate"
@input="$emit('end-date', $event)"
......@@ -69,15 +71,16 @@
<script>
import AbsenceReasonGroupSelect from "aleksis.apps.kolego/components/AbsenceReasonGroupSelect.vue";
import DateField from "aleksis.core/components/generic/forms/DateField.vue";
import DateTimeField from "aleksis.core/components/generic/forms/DateTimeField.vue";
import { persons } from "./absenceCreation.graphql";
import formRulesMixin from "aleksis.core/mixins/formRulesMixin.js";
import { DateTime } from "luxon";
export default {
name: "AbsenceCreationForm",
components: {
AbsenceReasonGroupSelect,
DateField,
DateTimeField,
},
mixins: [formRulesMixin],
emits: [
......@@ -113,5 +116,25 @@ export default {
required: true,
},
},
computed: {
maxStartTime() {
// Only if on the same day
const start = DateTime.fromISO(this.startDate);
const end = DateTime.fromISO(this.endDate);
if (start.day !== end.day) return;
return end.minus({ minutes: 5 }).toFormat("HH:mm");
},
minEndTime() {
// Only if on the same day
const start = DateTime.fromISO(this.startDate);
const end = DateTime.fromISO(this.endDate);
if (start.day !== end.day) return;
return start.plus({ minutes: 5 }).toFormat("HH:mm");
},
},
};
</script>
......@@ -15,6 +15,7 @@
<lesson-notes class="span-2" v-bind="documentationPartProps" />
<participation-list
v-if="documentation.canEditParticipationStatus"
:include-present="false"
class="participation-list"
v-bind="documentationPartProps"
......
......@@ -3,28 +3,40 @@ 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 PersonalNoteChip from "../personal_notes/PersonalNoteChip.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",
extends: MobileFullscreenDialog,
components: {
ExtraMarkButtons,
TardinessChip,
ExtraMarkChip,
AbsenceReasonChip,
AbsenceReasonGroupSelect,
AbsenceReasonButtons,
PersonalNotes,
PersonalNoteChip,
LessonInformation,
MessageBox,
MobileFullscreenDialog,
SecondaryActionButton,
SlideIterator,
TardinessField,
DialogCloseButton,
......@@ -37,6 +49,14 @@ export default {
loadSelected: false,
selected: [],
isExpanded: false,
markAsAbsentDay: {
showAlert: false,
num: 0,
reason: "no reason",
name: "nobody",
participationIDs: [],
loading: false,
},
};
},
props: {
......@@ -57,9 +77,9 @@ export default {
},
},
methods: {
handleMultipleAction(absenceReasonId) {
handleMultipleAction(field, id) {
this.loadSelected = true;
this.sendToServer(this.selected, "absenceReason", absenceReasonId);
this.sendToServer(this.selected, field, id);
this.$once("save", this.resetMultipleAction);
},
resetMultipleAction() {
......@@ -67,6 +87,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>
......@@ -104,6 +189,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
......@@ -138,18 +257,12 @@ export default {
small
:absence-reason="item.absenceReason"
/>
<v-chip
<personal-note-chip
v-for="note in item.notesWithNote"
:key="'text-note-note-overview-' + note.id"
:note="note"
small
>
<v-avatar left>
<v-icon small>mdi-note-outline</v-icon>
</v-avatar>
<span class="text-truncate" style="max-width: 30ch">
{{ note.note }}
</span>
</v-chip>
/>
<extra-mark-chip
v-for="note in item.notesWithExtraMark"
:key="'extra-mark-note-overview-' + note.id"
......@@ -209,11 +322,26 @@ export default {
<template #actions>
<v-scroll-y-reverse-transition>
<div v-show="selected.length > 0" class="full-width">
<h4>{{ $t("alsijil.coursebook.participation_status") }}</h4>
<absence-reason-buttons
class="mb-1"
allow-empty
empty-value="present"
:custom-absence-reasons="absenceReasons"
@input="handleMultipleAction"
@input="handleMultipleAction('absenceReason', $event)"
/>
<h4>{{ $t("alsijil.extra_marks.title_plural") }}</h4>
<extra-mark-buttons
@input="handleMultipleAction('extraMark', $event)"
/>
<h4>{{ $t("alsijil.personal_notes.tardiness") }}</h4>
<tardiness-field
v-bind="documentationPartProps"
:loading="loading"
:disabled="loading"
:value="0"
:participations="selected"
@input="handleMultipleAction('tardiness', $event)"
/>
</div>
</v-scroll-y-reverse-transition>
......
......@@ -57,6 +57,17 @@ export default {
);
},
},
computed: {
showLabel() {
return !!this.labelKey || !this.canOpenParticipation;
},
innerLabelKey() {
if (this.documentation.futureNoticeParticipationStatus) {
return "alsijil.coursebook.notes.future";
}
return this.labelKey;
},
},
};
</script>
......@@ -77,9 +88,9 @@ export default {
v-on="on"
@click="touchDocumentation"
>
<v-icon :left="!!labelKey">mdi-account-edit-outline</v-icon>
<template v-if="labelKey">
{{ $t(labelKey) }}
<v-icon :left="showLabel">mdi-account-edit-outline</v-icon>
<template v-if="showLabel">
{{ $t(innerLabelKey) }}
</template>
</v-chip>
</template>
......
......@@ -2,20 +2,20 @@
import { DateTime } from "luxon";
import documentationPartMixin from "../documentation/documentationPartMixin";
import ConfirmDialog from "aleksis.core/components/generic/dialogs/ConfirmDialog.vue";
import PositiveSmallIntegerField from "aleksis.core/components/generic/forms/PositiveSmallIntegerField.vue";
import formRulesMixin from "aleksis.core/mixins/formRulesMixin.js";
export default {
name: "TardinessField",
components: { ConfirmDialog, PositiveSmallIntegerField },
mixins: [documentationPartMixin],
components: { ConfirmDialog },
mixins: [documentationPartMixin, formRulesMixin],
props: {
value: {
type: Number,
default: null,
required: false,
},
participation: {
type: Object,
participations: {
type: Array,
required: true,
},
},
......@@ -27,6 +27,40 @@ export default {
let diff = lessonEnd.diff(lessonStart, "minutes");
return diff.toObject().minutes;
},
defaultTimes() {
const lessonStart = DateTime.fromISO(this.documentation.datetimeStart);
const lessonEnd = DateTime.fromISO(this.documentation.datetimeEnd);
const now = DateTime.now();
let current = [];
if (now >= lessonStart && now <= lessonEnd) {
const diff = parseInt(
now.diff(lessonStart, "minutes").toObject().minutes,
);
current.push({
text: diff,
value: diff,
current: true,
});
}
return current.concat([
{
text: 5,
value: 5,
},
{
text: 10,
value: 10,
},
{
text: 15,
value: 15,
},
]);
},
},
methods: {
lessonLengthRule(time) {
......@@ -46,14 +80,23 @@ export default {
cancel() {
this.saveValue(this.previousValue);
},
processValueObjectOptional(value) {
if (Object.hasOwn(value, "value")) {
return value.value;
}
return value;
},
set(newValue) {
if (!newValue) {
newValue = this.processValueObjectOptional(newValue);
if (!newValue || parseInt(newValue) === 0) {
// this is a DELETE action, show the dialog, ...
this.showDeleteConfirm = true;
return;
}
this.saveValue(newValue);
this.saveValue(parseInt(newValue));
},
},
data() {
......@@ -69,17 +112,40 @@ export default {
</script>
<template>
<positive-small-integer-field
<v-combobox
outlined
class="mt-1"
prepend-inner-icon="mdi-clock-alert-outline"
:suffix="$t('time.minutes')"
:label="$t('alsijil.personal_notes.tardiness')"
:rules="[lessonLengthRule]"
:rules="
$rules()
.isANumber.isAWholeNumber.isGreaterThan(0)
.build([lessonLengthRule])
.map((f) => (v) => f(this.processValueObjectOptional(v)))
"
:items="defaultTimes"
:value="value"
@change="set($event)"
v-bind="$attrs"
>
<template #item="{ item }">
<v-list-item-icon v-if="item.current">
<v-icon>mdi-shimmer</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>
{{
$tc(
item.current
? "alsijil.personal_notes.minutes_late_current"
: "time.minutes_n",
item.value,
)
}}
</v-list-item-title>
</v-list-item-content>
</template>
<template #append>
<confirm-dialog
v-model="showDeleteConfirm"
......@@ -93,13 +159,13 @@ export default {
{{
$t("alsijil.personal_notes.confirm_delete_tardiness", {
tardiness: previousValue,
name: participation.person.fullName,
name: participations.map((p) => p.person.firstName).join(", "),
})
}}
</template>
</confirm-dialog>
</template>
</positive-small-integer-field>
</v-combobox>
</template>
<style scoped>
......
......@@ -6,7 +6,7 @@ query persons {
}
}
query lessonsForPersons($persons: [ID]!, $start: Date!, $end: Date!) {
query lessonsForPersons($persons: [ID]!, $start: DateTime!, $end: DateTime!) {
items: lessonsForPersons(persons: $persons, start: $start, end: $end) {
id
lessons {
......@@ -31,8 +31,8 @@ query lessonsForPersons($persons: [ID]!, $start: Date!, $end: Date!) {
# Use absencesInputType?
mutation createAbsencesForPersons(
$persons: [ID]!
$start: Date!
$end: Date!
$start: DateTime!
$end: DateTime!
$comment: String
$reason: ID!
) {
......
......@@ -64,3 +64,14 @@ mutation touchDocumentation($documentationId: ID!) {
}
}
}
mutation extendParticipationStatuses($input: [ID]!) {
extendParticipationStatuses(input: $input) {
items: participations {
id
}
absences {
id
}
}
}
/**
* Mixin to provide shared functionality needed to send updated participation data to the server
*/
import { createPersonalNotes } from "../personal_notes/personal_notes.graphql";
import { updateParticipationStatuses } from "./participationStatus.graphql";
import mutateMixin from "aleksis.core/mixins/mutateMixin.js";
......@@ -18,11 +19,17 @@ export default {
fieldValue = {
tardiness: value,
};
} else if (field === "extraMark") {
// Too much different logic → own method
this.addExtraMarks(participations, value);
return;
} else {
console.error(`Wrong field '${field}' for sendToServer`);
return;
}
this.beforeSendToServer(participations, field, value);
this.mutate(
updateParticipationStatuses,
{
......@@ -46,9 +53,63 @@ 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
const participants = participations
.filter(
(participation) =>
!participation.notesWithExtraMark.some(
(note) => note.extraMark.id === extraMarkId,
),
)
.map((participation) => participation.person.id);
// CREATE new personal note
this.mutate(
createPersonalNotes,
{
input: participants.map((person) => ({
documentation: this.documentation.id,
person: person,
extraMark: extraMarkId,
})),
},
(storedDocumentations, incomingPersonalNotes) => {
const documentation = storedDocumentations.find(
(doc) => doc.id === this.documentation.id,
);
incomingPersonalNotes.forEach((note, index) => {
const participationStatus = documentation.participations.find(
(part) => part.person.id === participants[index],
);
participationStatus.notesWithExtraMark.push(note);
});
return storedDocumentations;
},
);
},
beforeSendToServer(_participations, _field, _value) {
// Noop hook
},
duringUpdateSendToServer(_participations, _field, _value, _incoming) {
// Noop hook
},
afterSendToServer(_participations, _field, _value) {
// Noop hook
},
},
};
......@@ -112,6 +112,9 @@ query documentationsForCoursebook(
canEdit
futureNotice
canDelete
futureNoticeParticipationStatus
canEditParticipationStatus
canViewParticipationStatus
}
}
......
......@@ -100,6 +100,6 @@ export default {
gap: 1em;
}
.vertical {
grid-template-columns: 1fr;
grid-template-columns: minmax(0, 1fr);
}
</style>
<template>
<v-card outlined dense rounded="lg" v-bind="$attrs" v-on="$listeners">
<div class="font-weight-medium mr-2">
{{ $t("alsijil.coursebook.summary.topic.label") }}:
</div>
<div class="text-truncate">{{ documentation.topic || "" }}</div>
<template v-if="documentation.topic">
<div class="font-weight-medium mr-2">
{{ $t("alsijil.coursebook.summary.topic.label") }}:
</div>
<div class="text-truncate">{{ documentation.topic }}</div>
</template>
<div class="font-weight-medium mr-2">
{{ $t("alsijil.coursebook.summary.homework.label") }}:
</div>
<div class="text-truncate">{{ documentation.homework || "" }}</div>
<template v-if="documentation.homework">
<div class="font-weight-medium mr-2">
{{ $t("alsijil.coursebook.summary.homework.label") }}:
</div>
<div class="text-truncate">{{ documentation.homework }}</div>
</template>
<div class="font-weight-medium mr-2">
{{ $t("alsijil.coursebook.summary.group_note.label") }}:
</div>
<div class="text-truncate">{{ documentation.groupNote || "" }}</div>
<template v-if="documentation.groupNote">
<div class="font-weight-medium mr-2">
{{ $t("alsijil.coursebook.summary.group_note.label") }}:
</div>
<div class="text-truncate">{{ documentation.groupNote }}</div>
</template>
</v-card>
</template>
......
......@@ -10,13 +10,23 @@
<v-card-title class="text-subtitle-2 pb-1 font-weight-medium">
{{ $t("alsijil.coursebook.summary.homework.label") }}
</v-card-title>
<v-card-text>{{ documentation.homework || "" }}</v-card-text>
<v-card-text>
{{
documentation.homework ||
$t("alsijil.coursebook.summary.homework.empty_yet")
}}
</v-card-text>
</v-card>
<v-card outlined dense rounded="lg">
<v-card-title class="text-subtitle-2 pb-1 font-weight-medium">
{{ $t("alsijil.coursebook.summary.group_note.label") }}
</v-card-title>
<v-card-text>{{ documentation.groupNote || "" }}</v-card-text>
<v-card-text>
{{
documentation.groupNote ||
$t("alsijil.coursebook.summary.group_note.empty")
}}
</v-card-text>
</v-card>
</div>
</template>
......
......@@ -10,6 +10,7 @@
:dialog-activator="activator"
:extra-marks="extraMarks"
:absence-reasons="absenceReasons"
:subjects="subjects"
/>
</template>
<!-- dialog view -> deactivate dialog -->
......@@ -18,6 +19,7 @@
v-bind="$attrs"
:extra-marks="extraMarks"
:absence-reasons="absenceReasons"
:subjects="subjects"
@close="popup = false"
/>
</mobile-fullscreen-dialog>
......@@ -47,6 +49,10 @@ export default {
type: Array,
required: true,
},
subjects: {
type: Array,
required: true,
},
},
};
</script>
......@@ -63,8 +63,8 @@ import SubjectChipSelectField from "aleksis.apps.cursus/components/SubjectChipSe
<template v-if="documentation.subject">
<subject-chip-select-field
v-if="documentation.canEdit"
:items="subjects"
:value="documentation.subject"
:enable-create="false"
:disabled="loading"
:loading="loading"
@input="editSubject"
......
<script setup>
import AbsenceReasonChip from "aleksis.apps.kolego/components/AbsenceReasonChip.vue";
import ExtraMarkChip from "../../extra_marks/ExtraMarkChip.vue";
import ExtraMarksNote from "../personal_notes/ExtraMarksNote.vue";
import TardinessChip from "../absences/TardinessChip.vue";
import PersonalNoteChip from "../personal_notes/PersonalNoteChip.vue";
import TextNoteCard from "../personal_notes/TextNoteCard.vue";
</script>
<template>
<div
class="d-flex align-center justify-space-between justify-md-end flex-wrap gap"
>
<v-chip dense color="success" outlined v-if="total > 0">
{{ $t("alsijil.coursebook.present_number", { present, total }) }}
</v-chip>
<absence-reason-chip
v-for="[reasonId, participations] in Object.entries(absences)"
:key="'reason-' + reasonId"
:absence-reason="participations[0].absenceReason"
dense
<div>
<div
class="d-flex align-center justify-space-between justify-md-end flex-wrap gap"
v-if="compact || documentation.canViewParticipationStatus"
>
<template #append>
<span
>:
<span>
{{
participations
.slice(0, 5)
.map((participation) => participation.person.firstName)
.join(", ")
}}
</span>
<span v-if="participations.length > 5">
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
+{{ participations.length - 5 }}
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
</span>
</span>
<v-chip
dense
color="success"
outlined
v-if="total > 0 && documentation.canViewParticipationStatus"
>
{{
$t("alsijil.coursebook.participations.present_number", {
present,
total,
})
}}
</v-chip>
<v-chip
dense
color="success"
outlined
@click="$emit('open')"
v-bind="dialogActivator.attrs"
v-on="dialogActivator.on"
v-else-if="
total == 1 &&
present == 1 &&
!documentation.canViewParticipationStatus
"
>
{{ $t("alsijil.coursebook.participations.present") }}
</v-chip>
<template v-if="documentation.canViewParticipationStatus">
<absence-reason-chip
v-for="[reasonId, participations] in Object.entries(absences)"
:key="'reason-' + reasonId"
:absence-reason="participations[0].absenceReason"
dense
>
<template #append>
<span
>:
<span>
{{
participations
.slice(0, 5)
.map((participation) => participation.person.firstName)
.join(", ")
}}
</span>
<span v-if="participations.length > 5">
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
+{{ participations.length - 5 }}
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
</span>
</span>
</template>
</absence-reason-chip>
</template>
<template v-else>
<absence-reason-chip
v-for="[reasonId, participations] in Object.entries(absences)"
:key="'reason-' + reasonId"
:absence-reason="participations[0].absenceReason"
dense
@click="$emit('open')"
v-bind="dialogActivator.attrs"
v-on="dialogActivator.on"
/>
</template>
</absence-reason-chip>
<extra-mark-chip
v-for="[markId, [mark, ...participations]] in Object.entries(
extraMarkChips,
)"
:key="'extra-mark-' + markId"
:extra-mark="mark"
dense
>
<template #append>
<span
>:
<span>
{{
participations
.slice(0, 5)
.map((participation) => participation.person.firstName)
.join(", ")
}}
</span>
<span v-if="participations.length > 5">
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
+{{ participations.length - 5 }}
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
</span>
</span>
<template v-if="documentation.canViewParticipationStatus">
<extra-mark-chip
v-for="[markId, [mark, ...participations]] in Object.entries(
extraMarkChips,
)"
:key="'extra-mark-' + markId"
:extra-mark="mark"
dense
>
<template #append>
<span
>:
<span>
{{
participations
.slice(0, 5)
.map((participation) => participation.person.firstName)
.join(", ")
}}
</span>
<span v-if="participations.length > 5">
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
+{{ participations.length - 5 }}
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
</span>
</span>
</template>
</extra-mark-chip>
</template>
<template v-else>
<extra-mark-chip
v-for="[markId, [mark, ...participations]] in Object.entries(
extraMarkChips,
)"
:key="'extra-mark-' + markId"
:extra-mark="mark"
dense
@click="$emit('open')"
v-bind="dialogActivator.attrs"
v-on="dialogActivator.on"
/>
</template>
</extra-mark-chip>
<tardiness-chip v-if="tardyParticipations.length > 0">
{{ $t("alsijil.personal_notes.late") }}
<template v-if="documentation.canViewParticipationStatus">
<tardiness-chip v-if="tardyParticipations.length > 0">
<template #default>
{{ $t("alsijil.personal_notes.late") }}
</template>
<template #append>
<span
>:
{{
tardyParticipations
.slice(0, 5)
.map((participation) => participation.person.firstName)
.join(", ")
}}
<template #append>
<span
>:
{{
tardyParticipations
.slice(0, 5)
.map((participation) => participation.person.firstName)
.join(", ")
}}
<span v-if="tardyParticipations.length > 5">
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
+{{ tardyParticipations.length - 5 }}
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
</span>
</span>
</template>
</tardiness-chip>
</template>
<template v-else>
<tardiness-chip
v-if="tardyParticipations.length > 0"
:tardiness="
tardyParticipations.length == 1
? tardyParticipations[0].tardiness
: undefined
"
@click="$emit('open')"
v-bind="dialogActivator.attrs"
v-on="dialogActivator.on"
/>
</template>
<span v-if="tardyParticipations.length > 5">
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
+{{ tardyParticipations.length - 5 }}
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
</span>
</span>
<template v-if="!documentation.canViewParticipationStatus && total == 1">
<personal-note-chip
v-for="note in documentation?.participations[0]?.notesWithNote"
:key="'text-note-note-' + note.id"
:note="note"
@click="$emit('open')"
v-bind="dialogActivator.attrs"
v-on="dialogActivator.on"
/>
</template>
</tardiness-chip>
<manage-students-trigger
:label-key="total == 0 ? 'alsijil.coursebook.notes.show_list' : ''"
v-bind="documentationPartProps"
/>
<manage-students-trigger
v-if="documentation.canEditParticipationStatus"
:label-key="manageStudentsLabelKey"
v-bind="documentationPartProps"
/>
</div>
<!-- not compact -->
<div class="main-body" v-else>
<template
v-if="
tardyParticipations.length > 0 || Object.entries(absences).length > 0
"
>
<v-divider />
<div
class="d-flex align-center justify-space-between justify-md-end flex-wrap gap"
>
<tardiness-chip
v-if="tardyParticipations.length > 0"
:tardiness="
tardyParticipations.length == 1
? tardyParticipations[0].tardiness
: undefined
"
/>
<absence-reason-chip
v-for="[reasonId, participations] in Object.entries(absences)"
:key="'reason-' + reasonId"
:absence-reason="participations[0].absenceReason"
dense
/>
</div>
</template>
<template v-if="total == 1">
<v-divider />
<extra-marks-note
v-bind="documentationPartProps"
:participation="documentation?.participations[0]"
:value="documentation?.participations[0].notesWithExtraMark"
:disabled="true"
/>
</template>
<template
v-if="
total == 1 &&
documentation?.participations[0]?.notesWithNote.length > 0
"
>
<v-divider />
<div>
<text-note-card
v-for="note in documentation?.participations[0]?.notesWithNote"
:key="'text-note-note-' + note.id"
:note="note"
/>
</div>
</template>
</div>
</div>
</template>
......@@ -161,6 +303,12 @@ export default {
tardyParticipations() {
return this.documentation.participations.filter((p) => p.tardiness);
},
manageStudentsLabelKey() {
if (this.total == 0) {
return "alsijil.coursebook.notes.show_list";
}
return "";
},
},
};
</script>
......@@ -169,4 +317,9 @@ export default {
.gap {
gap: 0.25em;
}
.main-body {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 1em;
}
</style>
......@@ -47,6 +47,13 @@ export default {
type: Array,
required: true,
},
/**
* Once loaded list of subjects to avoid excessive network and database queries
*/
subjects: {
type: Array,
required: true,
},
},
computed: {
......@@ -62,6 +69,7 @@ export default {
affectedQuery: this.affectedQuery,
extraMarks: this.extraMarks,
absenceReasons: this.absenceReasons,
subjects: this.subjects,
};
},
},
......
......@@ -88,7 +88,7 @@ export default {
:label="value.name"
:value="value.id"
v-model="model"
:disabled="loading"
:disabled="$attrs?.disabled || loading"
:true-value="true"
:false-value="false"
>
......