diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/MockPerson.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/MockPerson.vue deleted file mode 100644 index 98a346ea0b916640a50795c5336018d7cfb9b01e..0000000000000000000000000000000000000000 --- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/MockPerson.vue +++ /dev/null @@ -1,15 +0,0 @@ -<template> - <statistics-for-person-card :person="{ id: 100 }" /> -</template> - -<script> -import StatisticsForPersonCard from "./StatisticsForPersonCard.vue"; - -export default { - name: "MockPerson", - components: { - StatisticsForPersonCard, - }, - extends: "StatisticsForPersonCard", -}; -</script> diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForGroupTab.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForGroupTab.vue index 02aba3162b247bf8f9f5f7e2a528f183e510483b..e5eb12c62ccadcde90e51fa9b3f24865660bb830 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForGroupTab.vue +++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForGroupTab.vue @@ -20,6 +20,7 @@ #[`extraMarks.${index}.count`]="{ item }" > <extra-mark-chip + :key="extraMark.id" :extra-mark="extraMark" only-show-count dense @@ -34,6 +35,7 @@ #[`absenceReasons.${index}.count`]="{ item }" > <absence-reason-chip + :key="absenceReason.id" :absence-reason="absenceReason" only-show-count dense @@ -45,7 +47,8 @@ /> </template> - <template #person="{ item }"> + <!-- eslint-disable-next-line vue/valid-v-slot --> + <template #person.fullName="{ item }"> <person-chip :person="item.person" /> </template> @@ -56,7 +59,7 @@ </v-chip> <v-chip dense outlined> <v-icon left>mdi-sigma</v-icon> - {{ $tc("time.minutes_n", item.tardinessSum) }} + {{ $tc("time.minutes_n", item.tardinessSum, { n: $n(item.tardinessSum) }) }} </v-chip> </template> @@ -106,7 +109,7 @@ export default { return [ { text: this.$t("person.name"), - value: "person", + value: "person.fullName", }, ...this.absenceReasons.map((reason, index) => { return { @@ -129,6 +132,7 @@ export default { }, { value: "actions", + sortable: false, }, ]; }, diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonCard.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonCard.vue index a961e68928344566976051e9808bb542087fded5..751e6b90d15599ac113544b3698a30ac736e80ce 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonCard.vue +++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonCard.vue @@ -16,29 +16,29 @@ </v-card-title> <v-card-text> - <div class="d-flex mb-4" style="gap: 1em"> + <div class="grid"> <statistics-absences-card - class="flex-grow-1" + style="grid-area: absences" :absence-reasons="statistics.absenceReasons" :loading="$apollo.loading" /> <statistics-tardiness-card - class="flex-grow-1" + style="grid-area: tardinesses" :tardiness-sum="statistics.tardinessSum" :tardiness-count="statistics.tardinessCount" :loading="$apollo.loading" /> <statistics-extra-marks-card - class="flex-grow-1" + style="grid-area: extra_marks" :extra-marks="statistics.extraMarks" :loading="$apollo.loading" /> + <statistics-personal-notes-list + v-if="compact" + style="grid-area: personal_notes" + :loading="$apollo.loading" + /> </div> - <statistics-personal-notes-list - v-if="compact" - class="flex-grow-1" - :loading="$apollo.loading" - /> </v-card-text> </v-card> </template> @@ -102,12 +102,33 @@ export default { name: "alsijil.coursebook_statistics", params: { personId: this.person.id, - schoolTermId: this.statistics.schoolTerm, + schoolTermId: this.schoolTerm.id, }, // TODO: Add where we came from as get parameter if // meeting decided that own page. }); }, }, + computed: { + gridTemplateAreas() { + return this.compact + ? `"absences tardinesses extra_marks" + "personal_notes personal_notes personal_notes"` + : `"absences" "tardinesses" "extra_marks"`; + }, + gridTemplateColumnsNum() { + return this.compact ? 3 : 1; + }, + }, }; </script> + +<style scoped> +.grid { + display: grid; + max-width: 100%; + grid-template-columns: repeat(v-bind(gridTemplateColumnsNum), minmax(0, 1fr)); + grid-template-areas: v-bind(gridTemplateAreas); + gap: 0.5em; +} +</style> diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue index a15eec941683635098b9725753c626f89fb2ac23..e2f68e81e54072dc8bf90913b4941cae8dcab99d 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue +++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonPage.vue @@ -14,48 +14,61 @@ :enable-edit="false" :elevated="false" > - <template #item="{ item }"> - <v-list-item :key="item.id"> + <template #additionalActions> + <v-btn-toggle v-model="mode" mandatory color="secondary" rounded dense> + <v-btn outlined :value="MODE.PARTICIPATIONS"> + {{ $t("alsijil.coursebook.absences.absences") }} + </v-btn> + <v-btn outlined :value="MODE.PERSONAL_NOTES"> + {{ $t("alsijil.personal_notes.personal_notes") }} + </v-btn> + </v-btn-toggle> + </template> + <template #default="{ items }"> + <v-list-item v-for="item in items" :key="item.id" ripple> <v-list-item-content> - <v-list-item-title class="d-flex"> + <v-list-item-title> <!-- date & timeslot --> - <time :datetime="item.datetimeStart" class="text-no-wrap"> - {{ $d(DateTime.fromISO(item.datetimeStart), "shortDate") }} + <time :datetime="item.relatedDocumentation.datetimeStart" class="text-no-wrap"> + {{ $d($parseISODate(item.relatedDocumentation.datetimeStart), "short") }} </time> - <time :datetime="item.datetimeStart" class="text-no-wrap"> - {{ $d(DateTime.fromISO(item.datetimeStart), "shortTime") }} + + <time :datetime="item.relatedDocumentation.datetimeStart" class="text-no-wrap"> + {{ $d($parseISODate(item.relatedDocumentation.datetimeStart), "shortTime") }} </time> <span>-</span> - <time :datetime="item.datetimeEnd" class="text-no-wrap"> - {{ $d(DateTime.fromISO(item.datetimeEnd), "shortTime") }} + <time :datetime="item.relatedDocumentation.datetimeEnd" class="text-no-wrap"> + {{ $d($parseISODate(item.relatedDocumentation.datetimeEnd), "shortTime") }} </time> <!-- teacher --> - <person-chip :person="item.teacher" no-link /> + <person-chip + v-for="teacher in item.relatedDocumentation.teachers" + :person="teacher" + no-link + small + /> <!-- group --> - <div> + <span> {{ item.groupShortName }} - </div> + </span> <!-- subject --> - <subject-chip :subject="item.subject" /> - <v-spacer /> - <!-- chips: absences & extraMarks --> - <absence-reason-chip - v-for="absence in items.absences" - :key="absence.id" - :absenceReason="absence" - /> - <v-chip - v-for="extraMark in item.extraMarks" - :key="extraMark.id" - :value="extraMark.id" - :color="extraMark.colourBg" - :text-color="extraMark.colourFg" - > - {{ extraMark.name }} - </v-chip> + <subject-chip :subject="item.relatedDocumentation.subject" small /> </v-list-item-title> - <v-list-item-subtitle> item.personalNote </v-list-item-subtitle> + <v-list-item-subtitle> + {{ item.note }} + </v-list-item-subtitle> </v-list-item-content> + <v-list-item-action> + <!-- chips: absences & extraMarks --> + <absence-reason-chip + v-if="item.absenceReason" + :absenceReason="item.absenceReason" + /> + <extra-mark-chip + v-if="item.extraMark" + :extra-mark="item.extraMark" + /> + </v-list-item-action> </v-list-item> <v-divider></v-divider> </template> @@ -65,24 +78,33 @@ class="flex-shrink-1" :compact="false" :person="{ id: personId }" - :school-term="{ id: schoolTerm }" + :school-term="{ id: schoolTermId }" /> </div> </template> <script> +import AbsenceReasonChip from "aleksis.apps.kolego/components/AbsenceReasonChip.vue"; import SchoolTermField from "aleksis.core/components/school_term/SchoolTermField.vue"; import CRUDIterator from "aleksis.core/components/generic/CRUDIterator.vue"; import PersonChip from "aleksis.core/components/person/PersonChip.vue"; import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue"; import StatisticsForPersonCard from "./StatisticsForPersonCard.vue"; -import { documentationsByPerson } from "./statistics.graphql"; +import { participationsOfPerson, personalNotesForPerson } from "./statistics.graphql"; import { DateTime } from "luxon"; +import ExtraMarkChip from "../../extra_marks/ExtraMarkChip.vue"; + +const MODE = { + PARTICIPATIONS: "PARTICIPATIONS", + PERSONAL_NOTES: "PERSONAL_NOTES", +}; export default { name: "StatisticsForPersonPage", components: { + ExtraMarkChip, + AbsenceReasonChip, SchoolTermField, CRUDIterator, PersonChip, @@ -102,8 +124,7 @@ export default { }, data() { return { - schoolTerm: this.schoolTermId, - gqlQuery: documentationsByPerson, + mode: MODE.PARTICIPATIONS, }; }, computed: { @@ -113,6 +134,22 @@ export default { term: this.schoolTermId, }; }, + MODE() { + return MODE; + }, + schoolTerm: { + get() { + return this.schoolTermId; + }, + set(value) { + console.log("New SchoolTerm:", value); + } + }, + }, + methods: { + gqlQuery() { + return this.mode === MODE.PERSONAL_NOTES ? personalNotesForPerson : participationsOfPerson; + }, }, }; </script> diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsTardinessCard.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsTardinessCard.vue index 8921bcf059d0871e9682317eec8e3418893c62e7..275515f0f1b822d9b1da1dfe5fcbef264e242e6f 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsTardinessCard.vue +++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsTardinessCard.vue @@ -1,16 +1,27 @@ <template> <v-skeleton-loader v-if="loading" type="card" /> <v-card v-else class="text-center"> - <v-card-text> + <v-card-text + class="d-flex flex-column align-center justify-center fill-height" + > <div class="text-h2"> - {{ tardinessCount }} + {{ $n(tardinessCount) }} </div> <div class="text-subtitle-2"> - {{ $t("alsijil.tardiness.plural") }} + {{ $tc("alsijil.personal_notes.tardiness_n", tardinessCount) }} </div> - <div class="text-subtitle-2"> + <div class="text-caption"> <!-- TODO: Show average tardiness instead of sum like mock-up? --> - {{ tardinessSum }} + <div> + <v-icon small>mdi-sigma</v-icon> + {{ $tc("time.minutes_n", tardinessSum, { n: $n(tardinessSum) }) }} + </div> + <div> + <v-icon small>mdi-diameter-variant</v-icon> + {{ + $tc("time.minutes_n", tardinessSum / tardinessCount, { n: $n(tardinessSum/tardinessCount) }) + }} + </div> </div> </v-card-text> </v-card> diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql index d1f45309f7e9740b8965628fc45acc6bc8fa366e..38d8817b83e1a673f38d26352a682a407e5b3aa3 100644 --- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql +++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql @@ -36,33 +36,43 @@ query statisticsByPerson($person: ID!, $term: ID!) { } } -query documentationsByPerson($person: ID!, $term: ID) { - documentations: documentationsByPerson(person: $person, term: $term) { +query participationsOfPerson($person: ID!, $term: ID) { + items: participationsOfPerson(person: $person, term: $term) { id - datetimeStart - datetimeEnd - groupShortName - teacher { - id - shortName - fullName - avatarContentUrl - } - subject { - id - name - shortName - colourFg - colourBg - } - absences { + absenceReason { id shortName name colour default } - extraMarks { + tardiness + relatedDocumentation { + id + datetimeStart + datetimeEnd + teachers { + id + shortName + fullName + avatarContentUrl + } + subject { + id + name + shortName + colourFg + colourBg + } + } + } +} + +query personalNotesForPerson($person: ID!, $term: ID) { + items: personalNotesForPerson(person: $person, term: $term) { + id + note + extraMark { id shortName name @@ -70,7 +80,24 @@ query documentationsByPerson($person: ID!, $term: ID) { colourBg showInCoursebook } - personalNote + relatedDocumentation: documentation { + id + datetimeStart + datetimeEnd + teachers { + id + shortName + fullName + avatarContentUrl + } + subject { + id + name + shortName + colourFg + colourBg + } + } } } diff --git a/aleksis/apps/alsijil/frontend/index.js b/aleksis/apps/alsijil/frontend/index.js index 8af9024f8ea4d470224a10f6e15eded692469386..a39328f18a4345c0458f5dd4776d44579db96a8a 100644 --- a/aleksis/apps/alsijil/frontend/index.js +++ b/aleksis/apps/alsijil/frontend/index.js @@ -13,6 +13,21 @@ export const collectionItems = { import("./components/coursebook/statistics/StatisticsForGroupTab.vue"), }, ], + corePersonWidgets: [ + { + key: "core-person-widgets", + component: () => + import( + "./components/coursebook/statistics/StatisticsForPersonCard.vue" + ), + shouldDisplay: (person, currentSchoolTerm) => currentSchoolTerm != null, + colProps: { + cols: 12, + md: 6, + lg: 4, + }, + }, + ], }; export default { @@ -84,20 +99,6 @@ export default { permission: "alsijil.view_extramarks_rule", }, }, - { - path: "stats/", - component: () => - import("./components/coursebook/statistics/MockPerson.vue"), - name: "alsijil.coursebook_stats", - meta: { - inMenu: true, - icon: "mdi-book-education-outline", - iconActive: "mdi-book-education", - titleKey: "alsijil.coursebook.menu_title", - toolbarTitle: "alsijil.coursebook.menu_title", - permission: "alsijil.view_documentations_menu_rule", - }, - }, { path: "statistics/:personId/:schoolTermId/", component: () => diff --git a/aleksis/apps/alsijil/frontend/messages/de.json b/aleksis/apps/alsijil/frontend/messages/de.json index 352cae5489356e37bf750601f22adba9547ffab0..ab0fb17cd97b4f2a901e2f85b298bd1599e2d670 100644 --- a/aleksis/apps/alsijil/frontend/messages/de.json +++ b/aleksis/apps/alsijil/frontend/messages/de.json @@ -142,7 +142,8 @@ "minutes_late_current": "pünktlich (basierend auf der aktuellen Uhrzeit) | eine Minute zu spät (basierend auf der aktuellen Uhrzeit) | {n} Minuten zu spät (basierend auf der aktuellen Uhrzeit)", "note": "Notiz", "tardiness": "Verspätung", - "tardiness_plural": "Verspätungen" + "tardiness_plural": "Verspätungen", + "tardiness_n": "Verspätungen | Verspätung | Verspätungen" }, "persons": { "menu_title": "Meine Schüler*innen" diff --git a/aleksis/apps/alsijil/frontend/messages/en.json b/aleksis/apps/alsijil/frontend/messages/en.json index 0e1e8d43afa7c07788c5eefbed3875acaec1f7a1..827f782725e34a90926f9751dad295013fdd6d5b 100644 --- a/aleksis/apps/alsijil/frontend/messages/en.json +++ b/aleksis/apps/alsijil/frontend/messages/en.json @@ -76,6 +76,14 @@ "empty": "No group note" } }, + "statistics": { + "person_compact": { + "title": "Coursebook · Statistics" + }, + "person_page": { + "title": "Statistics" + } + }, "notes": { "show_list": "List of participants", "future": "Lesson is in the future" @@ -108,6 +116,7 @@ "present": "Present" }, "absences": { + "absences": "Absences", "action_for_selected": "Mark selected participant as: | Mark {count} selected participants as", "title": "Register absences", "button": "Register absences", @@ -130,6 +139,7 @@ "create_personal_note": "Add another note", "tardiness": "Tardiness", "tardiness_plural": "Tardiness", + "tardiness_n": "Tardiness | Tardiness | Tardiness", "late": "Late", "minutes_late": "on time | one minute late | {n} minutes late", "minutes_late_current": "on time (based on current time) | one minute late (based on current time) | {n} minutes late (based on current time)", @@ -146,7 +156,8 @@ "title": "Statistics" }, "person_view_details": "Details" - } + }, + "personal_notes": "Personal Notes" } }, "actions": { diff --git a/aleksis/apps/alsijil/schema/__init__.py b/aleksis/apps/alsijil/schema/__init__.py index 9e6ec22b596c7e1bf63d8b2b2c930f0856b622d0..ab2df1d2d6a63a116ce1bc53038b0ce049c2f75d 100644 --- a/aleksis/apps/alsijil/schema/__init__.py +++ b/aleksis/apps/alsijil/schema/__init__.py @@ -17,7 +17,7 @@ from aleksis.core.schema.person import PersonType from aleksis.core.util.core_helpers import get_site_preferences, has_person from ..model_extensions import annotate_person_statistics_for_school_term -from ..models import Documentation +from ..models import Documentation, ParticipationStatus, NewPersonalNote from .absences import ( AbsencesForPersonsCreateMutation, ) @@ -36,16 +36,15 @@ from .extra_marks import ( from .participation_status import ( ExtendParticipationStatusToAbsenceBatchMutation, ParticipationStatusBatchPatchMutation, + ParticipationStatusType, ) from .personal_note import ( PersonalNoteBatchCreateMutation, PersonalNoteBatchDeleteMutation, PersonalNoteBatchPatchMutation, + PersonalNoteType, ) -from .statistics import ( - DocumentationByPersonType, - StatisticsByPersonType, -) +from .statistics import StatisticsByPersonType class Query(graphene.ObjectType): @@ -84,8 +83,13 @@ class Query(graphene.ObjectType): person=graphene.ID(required=True), term=graphene.ID(required=True), ) - documentations_by_person = graphene.List( - DocumentationByPersonType, + participations_of_person = graphene.List( + ParticipationStatusType, + person=graphene.ID(required=True), + term=graphene.ID(required=False), + ) + personal_notes_for_person = graphene.List( + PersonalNoteType, person=graphene.ID(required=True), term=graphene.ID(required=False), ) @@ -261,9 +265,14 @@ class Query(graphene.ObjectType): ).first() @staticmethod - def resolve_documentations_by_person(root, info, person, term=None): - # TODO: Annotate person with necessary information for term. - return Person.objects.get(id=person) + def resolve_participations_of_person(root, info, person, term=None): + # TODO: only current term + return ParticipationStatus.objects.filter(person=person, absence_reason__isnull=False) + + @staticmethod + def resolve_personal_notes_for_person(root, info, person, term=None): + # TODO: only current term + return NewPersonalNote.objects.filter(person=person) @staticmethod def resolve_statistics_by_group(root, info, group, term=None): diff --git a/aleksis/apps/alsijil/schema/statistics.py b/aleksis/apps/alsijil/schema/statistics.py index a6292433accad63081fed3d49e2e944071ec09cb..976f25ebb32b50c9152fb86d23acf17011d7a89f 100644 --- a/aleksis/apps/alsijil/schema/statistics.py +++ b/aleksis/apps/alsijil/schema/statistics.py @@ -1,3 +1,5 @@ +from django.utils import timezone + import graphene from aleksis.apps.cursus.models import Subject @@ -63,42 +65,3 @@ class StatisticsByPersonType(graphene.ObjectType): dict(extra_mark=extra_mark, count=getattr(root, extra_mark.count_label)) for extra_mark in ExtraMark.objects.all() ] - - -class DocumentationByPersonType(graphene.ObjectType): - id = graphene.ID() - datetime_start = graphene.Date() - datetime_end = graphene.Date() - group_short_name = graphene.String() - teacher = graphene.Field(PersonType) - subject = graphene.Field(SubjectType) - absences = graphene.List(AbsenceReasonType) - extra_marks = graphene.List(ExtraMarkType) - personal_note = graphene.String() - - def resolve_id(root, info): - return 1 - - def resolve_datetime_start(root, info): - return "2024-05-27T09:00:00+00:00" - - def resolve_datetime_end(root, info): - return "2024-05-27T10:00:00+00:00" - - def resolve_group_short_name(root, info): - return "11b" - - def resolve_teacher(root, info): - return Person.objects.get(id=63) - - def resolve_subject(root, info): - return Subject.objects.get(id=1) - - def resolve_absences(root, info): - return AbsenceReason.objects.all() - - def resolve_extra_marks(root, info): - return ExtraMark.objects.all() - - def resolve_personal_note(root, info): - return "All is well:)"