Skip to content
Snippets Groups Projects
Commit 0e5225db authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Merge branch '256-add-simple-course-book-list' into 'master'

Resolve "Add simple course book list"

Closes #256 and #63

See merge request !350
parents b9a45f25 bce32819
No related branches found
No related tags found
3 merge requests!352Draft: Resolve "Add dialog with each lesson's students",!350Resolve "Add simple course book list",!339Draft: Resolve "Migrate to new data model"
Pipeline #181873 canceled
Showing
with 1726 additions and 336 deletions
......@@ -9,8 +9,28 @@ and this project adheres to `Semantic Versioning`_.
Unreleased
----------
Notable, breaking changes
~~~~~~~~~~~~~~~~~~~~~~~~~
Starting from the class register core functionality, Alsijil is getting a entire rewrite
of both its frontend and backend. The models formerly used for lesson documentation, notably
`LessonDocumentation` and `PersonalNote` are replaced by new ones based on the calendar framework
provided by `AlekSIS-Core` and the absense framework provided by `AlekSIS-App-Kolego`. The legacy
views providing management functionality for those legacy models are not available anymore. Currently,
there exists no migration path away from legacy data.
Changed
~~~~~~~
* Modern rewrite of class register/coursebook, both in the frontend and the backend
* Several legacy class register views were consolidated in a modern frontend (coursebook).
* [Dev] The `LessonDocumentation` model is replaced with the `Documentation` model, based on the calendar framework.
* [Dev] The `PersonalNote` model is replaced with the `NewPersonalNote` model.
* [Dev] Participation status documentation is taken over by the new `Participation` model.
Fixed
~~~~~
* Migrating failed due to an incorrect field reference.
`3.0`_ - 2023-05-15
......
<template>
<c-r-u-d-iterator
i18n-key="alsijil.coursebook"
:gql-query="gqlQuery"
:gql-additional-query-args="gqlQueryArgs"
:enable-create="false"
:enable-edit="false"
:elevated="false"
@lastQuery="lastQuery = $event"
ref="iterator"
fixed-header
disable-pagination
hide-default-footer
use-deep-search
>
<template #additionalActions="{ attrs, on }">
<coursebook-filters v-model="filters" />
</template>
<template #default="{ items }">
<coursebook-loader />
<coursebook-day
v-for="{ date, docs, first, last } in groupDocsByDay(items)"
v-intersect="{
handler: intersectHandler(date, first, last),
options: {
rootMargin: '-' + topMargin + 'px 0px 0px 0px',
threshold: [0, 1],
},
}"
:date="date"
:docs="docs"
:lastQuery="lastQuery"
:focus-on-mount="initDate && initDate.toMillis() === date.toMillis()"
@init="transition"
:key="'day-' + date"
ref="days"
/>
<coursebook-loader />
<date-select-footer
:value="currentDate"
@input="gotoDate"
@prev="gotoPrev"
@next="gotoNext"
/>
</template>
<template #loading>
<coursebook-loader :number-of-days="10" :number-of-docs="5" />
</template>
<template #no-data>
<CoursebookEmptyMessage icon="mdi-book-off-outline">
{{ $t("alsijil.coursebook.no_data") }}
</CoursebookEmptyMessage>
</template>
<template #no-results>
<CoursebookEmptyMessage icon="mdi-book-alert-outline">
{{
$t("alsijil.coursebook.no_results", { search: $refs.iterator.search })
}}
</CoursebookEmptyMessage>
</template>
</c-r-u-d-iterator>
</template>
<script>
import CRUDIterator from "aleksis.core/components/generic/CRUDIterator.vue";
import DateSelectFooter from "aleksis.core/components/generic/DateSelectFooter.vue";
import CoursebookDay from "./CoursebookDay.vue";
import { DateTime, Interval } from "luxon";
import { documentationsForCoursebook } from "./coursebook.graphql";
import CoursebookFilters from "./CoursebookFilters.vue";
import CoursebookLoader from "./CoursebookLoader.vue";
import CoursebookEmptyMessage from "./CoursebookEmptyMessage.vue";
export default {
name: "Coursebook",
components: {
CoursebookEmptyMessage,
CoursebookFilters,
CoursebookLoader,
CRUDIterator,
DateSelectFooter,
CoursebookDay,
},
props: {
filterType: {
type: String,
required: true,
},
objId: {
type: [Number, String],
required: false,
default: null,
},
objType: {
type: String,
required: false,
default: null,
},
/**
* Number of consecutive to load at once
* This number of days is initially loaded and loaded
* incrementally while scrolling.
*/
dayIncrement: {
type: Number,
required: false,
default: 7,
},
/**
* Margin from coursebook list to top of viewport in pixels
*/
topMargin: {
type: Number,
required: false,
default: 165,
},
},
data() {
return {
gqlQuery: documentationsForCoursebook,
lastQuery: null,
dateStart: "",
dateEnd: "",
// Placeholder values while query isn't completed yet
groups: [],
courses: [],
incomplete: false,
ready: false,
initDate: false,
currentDate: "",
hashUpdater: false,
};
},
computed: {
// Assertion: Should only fire on page load or selection change.
// Resets date range.
gqlQueryArgs() {
return {
own: this.filterType === "all" ? false : true,
objId: this.objId ? Number(this.objId) : undefined,
objType: this.objType?.toUpperCase(),
dateStart: this.dateStart,
dateEnd: this.dateEnd,
incomplete: !!this.incomplete,
};
},
filters: {
get() {
return {
objType: this.objType,
objId: this.objId,
filterType: this.filterType,
incomplete: this.incomplete,
};
},
set(selectedFilters) {
if (Object.hasOwn(selectedFilters, "incomplete")) {
this.incomplete = selectedFilters.incomplete;
} else if (
Object.hasOwn(selectedFilters, "filterType") ||
Object.hasOwn(selectedFilters, "objId") ||
Object.hasOwn(selectedFilters, "objType")
) {
this.$router.push({
name: "alsijil.coursebook",
params: {
filterType: selectedFilters.filterType
? selectedFilters.filterType
: this.filterType,
objType: selectedFilters.objType,
objId: selectedFilters.objId,
},
hash: this.$route.hash,
});
// computed should not have side effects
// but this was actually done before filters was refactored into
// its own component
this.resetDate();
// might skip query until both set = atomic
}
},
},
},
methods: {
resetDate(toDate) {
// Assure current date
console.log("Resetting date range", this.$route.hash);
this.currentDate = toDate || this.$route.hash?.substring(1);
if (!this.currentDate) {
console.log("Set default date");
this.setDate(DateTime.now().toISODate());
}
const date = DateTime.fromISO(this.currentDate);
this.initDate = date;
this.dateStart = date.minus({ days: this.dayIncrement }).toISODate();
this.dateEnd = date.plus({ days: this.dayIncrement }).toISODate();
},
transition() {
this.initDate = false;
this.ready = true;
},
groupDocsByDay(docs) {
// => {dt: {date: dt, docs: doc ...} ...}
const docsByDay = docs.reduce((byDay, doc) => {
// This works with dummy. Does actual doc have dateStart instead?
const day = DateTime.fromISO(doc.datetimeStart).startOf("day");
byDay[day] ??= { date: day, docs: [] };
byDay[day].docs.push(doc);
return byDay;
}, {});
// => [{date: dt, docs: doc ..., idx: idx, lastIdx: last-idx} ...]
// sorting is necessary since backend can send docs unordered
return Object.keys(docsByDay)
.sort()
.map((key, idx, { length }) => {
const day = docsByDay[key];
day.first = idx === 0;
const lastIdx = length - 1;
day.last = idx === lastIdx;
return day;
});
},
// docsByDay: {dt: [dt doc ...] ...}
fetchMore(from, to, then) {
console.log("fetching", from, to);
this.lastQuery.fetchMore({
variables: {
dateStart: from,
dateEnd: to,
},
// Transform the previous result with new data
updateQuery: (previousResult, { fetchMoreResult }) => {
console.log("Received more");
then();
return { items: previousResult.items.concat(fetchMoreResult.items) };
},
});
},
setDate(date) {
this.currentDate = date;
if (!this.hashUpdater) {
this.hashUpdater = window.requestIdleCallback(() => {
if (!(this.$route.hash.substring(1) === this.currentDate)) {
this.$router.replace({ hash: this.currentDate });
}
this.hashUpdater = false;
});
}
},
fixScrollPos(height, top) {
this.$nextTick(() => {
if (height < document.documentElement.scrollHeight) {
document.documentElement.scrollTop =
document.documentElement.scrollHeight - height + top;
this.ready = true;
} else {
// Update top, could have changed in the meantime.
this.fixScrollPos(height, document.documentElement.scrollTop);
}
});
},
intersectHandler(date, first, last) {
let once = true;
return (entries) => {
const entry = entries[0];
if (entry.isIntersecting) {
if (entry.boundingClientRect.top <= this.topMargin || first) {
console.log("@ ", date.toISODate());
this.setDate(date.toISODate());
}
if (once && this.ready && first) {
console.log("load up", date.toISODate());
this.ready = false;
this.fetchMore(
date.minus({ days: this.dayIncrement }).toISODate(),
date.minus({ days: 1 }).toISODate(),
() => {
this.fixScrollPos(
document.documentElement.scrollHeight,
document.documentElement.scrollTop,
);
},
);
once = false;
} else if (once && this.ready && last) {
console.log("load down", date.toISODate());
this.ready = false;
this.fetchMore(
date.plus({ days: 1 }).toISODate(),
date.plus({ days: this.dayIncrement }).toISODate(),
() => {
this.ready = true;
},
);
once = false;
}
}
};
},
// Improve me?
// The navigation logic could be a bit simpler if the current days
// where known as a sorted array (= result of groupDocsByDay) But
// then the list would need its own component and this gets rather
// complicated. Then the calendar could also show the present days
// / gray out the missing.
//
// Next two: arg date is ts object
findPrev(date) {
return this.$refs.days
.map((day) => day.date)
.sort()
.reverse()
.find((date2) => date2 < date);
},
findNext(date) {
return this.$refs.days
.map((day) => day.date)
.sort()
.find((date2) => date2 > date);
},
gotoDate(date) {
const present = this.$refs.days.find(
(day) => day.date.toISODate() === date,
);
if (present) {
// React immediatly -> smoother navigation
// Also intersect handler does not always react to scrollIntoView
this.setDate(date);
present.focus("smooth");
} else if (
!this.findPrev(DateTime.fromISO(date)) ||
!this.findNext(DateTime.fromISO(date))
) {
this.resetDate(date);
}
},
gotoPrev() {
const prev = this.findPrev(DateTime.fromISO(this.currentDate));
if (prev) {
this.gotoDate(prev.toISODate());
}
},
gotoNext() {
const next = this.findNext(DateTime.fromISO(this.currentDate));
if (next) {
this.gotoDate(next.toISODate());
}
},
},
created() {
this.resetDate();
},
};
</script>
<style>
.max-width {
max-width: 25rem;
}
</style>
<template>
<v-list-item :style="{ scrollMarginTop: '145px' }" two-line>
<v-list-item-content>
<v-subheader class="text-h6">{{
$d(date, "dateWithWeekday")
}}</v-subheader>
<v-list max-width="100%" class="pt-0 mt-n1">
<v-list-item
v-for="doc in docs"
:key="'documentation-' + (doc.oldId || doc.id)"
>
<documentation-modal
:documentation="doc"
:affected-query="lastQuery"
/>
</v-list-item>
</v-list>
</v-list-item-content>
</v-list-item>
</template>
<script>
import DocumentationModal from "./documentation/DocumentationModal.vue";
export default {
name: "CoursebookDay",
components: {
DocumentationModal,
},
props: {
date: {
type: Object,
required: true,
},
docs: {
type: Array,
required: true,
},
lastQuery: {
type: Object,
required: true,
},
focusOnMount: {
type: Boolean,
required: false,
default: false,
},
},
emits: ["init"],
methods: {
focus(how) {
this.$el.scrollIntoView({
behavior: how,
block: "start",
inline: "nearest",
});
console.log("focused @", this.date.toISODate());
},
},
mounted() {
if (this.focusOnMount) {
this.$nextTick(this.focus("instant"));
this.$emit("init");
}
},
};
</script>
<template>
<v-list-item>
<v-list-item-content
class="d-flex justify-center align-center flex-column full-width"
>
<div class="mb-4">
<v-icon large color="primary">{{ icon }}</v-icon>
</div>
<v-list-item-title>
<slot></slot>
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</template>
<script>
export default {
name: "CoursebookEmptyMessage",
props: {
icon: {
type: String,
default: "mdi-book-alert-outline",
},
},
};
</script>
<template>
<div class="d-flex flex-grow-1 justify-end">
<v-autocomplete
:items="selectable"
item-text="name"
clearable
return-object
filled
dense
hide-details
:placeholder="$t('alsijil.coursebook.filter.filter_for_obj')"
:loading="selectLoading"
:value="currentObj"
@input="selectObject"
@click:clear="selectObject"
class="max-width"
/>
<div class="ml-6">
<v-switch
:loading="selectLoading"
:label="$t('alsijil.coursebook.filter.own')"
:input-value="value.filterType === 'my'"
@change="selectFilterType($event)"
dense
inset
hide-details
/>
<v-switch
:loading="selectLoading"
:label="$t('alsijil.coursebook.filter.missing')"
:input-value="value.incomplete"
@change="
$emit('input', {
incomplete: $event,
})
"
dense
inset
hide-details
/>
</div>
</div>
</template>
<script>
import { coursesOfPerson, groupsByPerson } from "./coursebook.graphql";
export default {
name: "CoursebookFilters",
data() {
return {
// Placeholder values while query isn't completed yet
groups: [],
courses: [],
};
},
props: {
value: {
type: Object,
required: true,
},
},
emits: ["input"],
apollo: {
groups: {
query: groupsByPerson,
},
courses: {
query: coursesOfPerson,
},
},
computed: {
selectable() {
return [
{ header: this.$t("alsijil.coursebook.filter.groups") },
...this.groups.map((group) => ({ type: "group", ...group })),
{ header: this.$t("alsijil.coursebook.filter.courses") },
...this.courses.map((course) => ({ type: "course", ...course })),
];
},
selectLoading() {
return (
this.$apollo.queries.groups.loading ||
this.$apollo.queries.courses.loading
);
},
currentObj() {
return this.selectable.find(
(o) => o.type === this.value.objType && o.id === this.value.objId,
);
},
},
methods: {
selectObject(selection) {
this.$emit("input", {
objType: selection ? selection.type : null,
objId: selection ? selection.id : null,
});
},
selectFilterType(switchValue) {
this.$emit("input", {
filterType: switchValue ? "my" : "all",
objType: this.value.objType,
objId: this.value.objId,
});
},
},
};
</script>
<template>
<div>
<v-list-item v-for="i in numberOfDays" :key="'i-' + i">
<v-list-item-content>
<v-list-item-title>
<v-skeleton-loader type="heading" />
</v-list-item-title>
<v-list max-width="100%">
<v-list-item v-for="j in numberOfDocs" :key="'j-' + j">
<DocumentationLoader />
</v-list-item>
</v-list>
</v-list-item-content>
</v-list-item>
</div>
</template>
<script>
import DocumentationLoader from "./documentation/DocumentationLoader.vue";
export default {
name: "CoursebookLoader",
components: { DocumentationLoader },
props: {
numberOfDays: {
type: Number,
required: false,
default: 1,
},
numberOfDocs: {
type: Number,
required: false,
default: 1,
},
},
};
</script>
query groupsByPerson {
groups: groupsByPerson {
id
name
}
}
query coursesOfPerson {
courses: coursesOfPerson {
id
name
groups {
id
name
}
}
}
query documentationsForCoursebook(
$own: Boolean!
$objId: ID
$objType: String
$dateStart: Date!
$dateEnd: Date!
$incomplete: Boolean
) {
items: documentationsForCoursebook(
own: $own
objId: $objId
objType: $objType
dateStart: $dateStart
dateEnd: $dateEnd
incomplete: $incomplete
) {
id
course {
id
name
}
amends {
id
amends {
id
teachers {
id
shortName
fullName
avatarContentUrl
}
subject {
id
name
shortName
colourFg
colourBg
}
}
cancelled
}
teachers {
id
shortName
fullName
avatarContentUrl
}
subject {
id
name
shortName
colourFg
colourBg
}
topic
homework
groupNote
datetimeStart
datetimeEnd
dateStart
dateEnd
oldId
canEdit
futureNotice
canDelete
}
}
mutation createOrUpdateDocumentations($input: [DocumentationInputType]!) {
createOrUpdateDocumentations(input: $input) {
items: documentations {
id
topic
homework
groupNote
oldId
}
}
}
<template>
<v-card :class="{ 'my-1 full-width': true, 'd-flex flex-column': !compact }">
<v-card-title v-if="!compact">
<lesson-information v-bind="documentationPartProps" />
</v-card-title>
<v-card-text
class="full-width main-body"
:class="{
vertical: !compact || $vuetify.breakpoint.mobile,
'pa-2': compact,
}"
>
<lesson-information v-if="compact" v-bind="documentationPartProps" />
<lesson-summary
ref="summary"
v-bind="{ ...$attrs, ...documentationPartProps }"
:is-create="false"
:gql-patch-mutation="documentationsMutation"
@open="$emit('open')"
@loading="loading = $event"
@save="$emit('close')"
/>
<lesson-notes v-bind="documentationPartProps" />
</v-card-text>
<v-spacer />
<v-divider />
<v-card-actions v-if="!compact">
<v-spacer />
<cancel-button
v-if="documentation.canEdit"
@click="$emit('close')"
:disabled="loading"
/>
<save-button
v-if="documentation.canEdit"
@click="save"
:loading="loading"
/>
<cancel-button
v-if="!documentation.canEdit"
i18n-key="actions.close"
@click="$emit('close')"
/>
</v-card-actions>
</v-card>
</template>
<script>
import LessonInformation from "./LessonInformation.vue";
import LessonSummary from "./LessonSummary.vue";
import LessonNotes from "./LessonNotes.vue";
import SaveButton from "aleksis.core/components/generic/buttons/SaveButton.vue";
import CancelButton from "aleksis.core/components/generic/buttons/CancelButton.vue";
import { createOrUpdateDocumentations } from "../coursebook.graphql";
import documentationPartMixin from "./documentationPartMixin";
export default {
name: "Documentation",
components: {
LessonInformation,
LessonSummary,
LessonNotes,
SaveButton,
CancelButton,
},
emits: ["open", "close"],
mixins: [documentationPartMixin],
data() {
return {
loading: false,
documentationsMutation: createOrUpdateDocumentations,
};
},
methods: {
save() {
this.$refs.summary.save();
this.$emit("close");
},
},
};
</script>
<style scoped>
.main-body {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1em;
}
.vertical {
grid-template-columns: 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>
<div class="font-weight-medium mr-2">
{{ $t("alsijil.coursebook.summary.homework.label") }}:
</div>
<div class="text-truncate">{{ documentation.homework || "" }}</div>
<div class="font-weight-medium mr-2">
{{ $t("alsijil.coursebook.summary.group_note.label") }}:
</div>
<div class="text-truncate">{{ documentation.groupNote || "" }}</div>
</v-card>
</template>
<script>
export default {
name: "DocumentationCompactDetails",
props: {
documentation: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div v-bind="$attrs" v-on="$listeners">
<v-card outlined dense rounded="lg" class="mb-2">
<v-card-title class="text-subtitle-2 pb-1 font-weight-medium">
{{ $t("alsijil.coursebook.summary.topic.label") }}
</v-card-title>
<v-card-text>{{ documentation.topic || "" }}</v-card-text>
</v-card>
<v-card outlined dense rounded="lg" class="mb-2">
<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>
<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>
</div>
</template>
<script>
export default {
name: "DocumentationFullDetails",
props: {
documentation: {
type: Object,
required: true,
},
},
};
</script>
<template>
<v-card class="my-2 full-width">
<div class="full-width d-flex flex-column align-stretch flex-md-row">
<v-card-text>
<v-skeleton-loader
type="avatar, heading, chip"
class="d-flex full-width align-center gap"
height="100%"
/>
</v-card-text>
<v-card-text>
<v-skeleton-loader
type="heading@2"
class="d-flex full-width align-center gap"
height="100%"
/>
</v-card-text>
<v-card-text>
<v-skeleton-loader
type="chip@3"
class="d-flex full-width align-center justify-end gap"
height="100%"
/>
</v-card-text>
</div>
</v-card>
</template>
<script>
export default {
name: "DocumentationLoader",
};
</script>
<!-- Wrapper around Documentation.vue -->
<!-- That uses it either as list item or as editable modal dialog. -->
<template>
<mobile-fullscreen-dialog v-model="popup" max-width="500px">
<template #activator="activator">
<!-- list view -> activate dialog -->
<documentation compact v-bind="$attrs" :dialog-activator="activator" />
</template>
<!-- dialog view -> deactivate dialog -->
<!-- cancel | save (through lesson-summary) -->
<documentation v-bind="$attrs" @close="popup = false" />
</mobile-fullscreen-dialog>
</template>
<script>
import MobileFullscreenDialog from "aleksis.core/components/generic/dialogs/MobileFullscreenDialog.vue";
import Documentation from "./Documentation.vue";
export default {
name: "DocumentationModal",
components: {
MobileFullscreenDialog,
Documentation,
},
data() {
return {
popup: false,
};
},
};
</script>
<template>
<v-tooltip bottom>
<template #activator="{ on, attrs }">
<v-icon
:color="currentStatus?.color"
class="mr-md-4"
v-on="on"
v-bind="attrs"
>{{ currentStatus?.icon }}</v-icon
>
</template>
<span>{{ currentStatus?.text }}</span>
</v-tooltip>
</template>
<script>
import documentationPartMixin from "./documentationPartMixin";
import { DateTime } from "luxon";
export default {
name: "DocumentationStatus",
mixins: [documentationPartMixin],
data() {
return {
statusChoices: [
{
name: "available",
text: this.$t("alsijil.coursebook.status.available"),
icon: "$success",
color: "success",
},
{
name: "missing",
text: this.$t("alsijil.coursebook.status.missing"),
icon: "$warning",
color: "error",
},
{
name: "running",
text: this.$t("alsijil.coursebook.status.running"),
icon: "mdi-play-outline",
color: "warning",
},
{
name: "substitution",
text: this.$t("alsijil.coursebook.status.substitution"),
icon: "$info",
color: "warning",
},
{
name: "cancelled",
text: this.$t("alsijil.coursebook.status.cancelled"),
icon: "mdi-cancel",
color: "error",
},
{
name: "pending",
text: this.$t("alsijil.coursebook.status.pending"),
icon: "mdi-clipboard-clock-outline",
color: "blue",
},
],
statusTimeout: null,
currentStatusName: "",
};
},
computed: {
currentStatus() {
return this.statusChoices.find((s) => s.name === this.currentStatusName);
},
documentationDateTimeStart() {
return DateTime.fromISO(this.documentation.datetimeStart);
},
documentationDateTimeEnd() {
return DateTime.fromISO(this.documentation.datetimeEnd);
},
},
methods: {
updateStatus() {
if (this.documentation?.amends.cancelled) {
this.currentStatusName = "cancelled";
} else if (this.documentation.topic) {
this.currentStatusName = "available";
} else if (DateTime.now() > this.documentationDateTimeEnd) {
this.currentStatusName = "missing";
} else if (this.documentation?.amends.amends) {
this.currentStatusName = "substitution";
} else if (
DateTime.now() > this.documentationDateTimeStart &&
DateTime.now() < this.documentationDateTimeEnd
) {
this.currentStatusName = "running";
} else {
this.currentStatusName = "pending";
}
},
},
watch: {
documentation: {
handler() {
this.updateStatus();
},
deep: true,
},
},
mounted() {
this.updateStatus();
if (DateTime.now() < this.documentationDateTimeStart) {
this.statusTimeout = setTimeout(
this.updateStatus,
this.documentationDateTimeStart
.diff(DateTime.now(), "seconds")
.toObject(),
);
} else if (DateTime.now() < this.documentationDateTimeEnd) {
this.statusTimeout = setTimeout(
this.updateStatus,
this.documentationDateTimeEnd
.diff(DateTime.now(), "seconds")
.toObject(),
);
}
},
beforeDestroy() {
if (this.statusTimeout) {
clearTimeout(this.statusTimeout);
}
},
};
</script>
<script setup>
import DocumentationStatus from "./DocumentationStatus.vue";
import PersonChip from "aleksis.core/components/person/PersonChip.vue";
</script>
<template>
<div :class="{ 'full-width grid': true, 'large-grid': largeGrid }">
<div class="d-flex">
<documentation-status v-if="compact" v-bind="documentationPartProps" />
<div :class="{ 'text-right d-flex flex-column fit-content': largeGrid }">
<time :datetime="documentation.datetimeStart" class="text-no-wrap">
{{ $d(toDateTime(documentation.datetimeStart), "shortTime") }}
</time>
<span v-if="!largeGrid"></span>
<time :datetime="documentation.datetimeEnd" class="text-no-wrap">
{{ $d(toDateTime(documentation.datetimeEnd), "shortTime") }}
</time>
</div>
</div>
<span
:class="{
'text-right': !largeGrid,
'text-subtitle-1': largeGrid,
'font-weight-medium': largeGrid,
}"
>
{{ documentation.course?.name }}
</span>
<div
:class="{
'd-flex align-center flex-wrap gap': true,
'justify-center': largeGrid,
'justify-start': !largeGrid,
}"
>
<subject-chip
v-if="documentation.subject"
:subject="documentation.subject"
v-bind="compact ? dialogActivator.attrs : {}"
v-on="compact ? dialogActivator.on : {}"
/>
<subject-chip
v-if="
documentation?.amends?.amends?.subject &&
documentation.amends.amends.subject.id !== documentation.subject.id
"
:subject="documentation.amends.amends.subject"
v-bind="compact ? dialogActivator.attrs : {}"
v-on="compact ? dialogActivator.on : {}"
class="text-decoration-line-through"
disabled
/>
</div>
<div
:class="{
'd-flex align-center flex-wrap gap': true,
'justify-end': !largeGrid,
}"
>
<person-chip
v-for="teacher in documentation.teachers"
:key="documentation.id + '-teacher-' + teacher.id"
:person="teacher"
no-link
v-bind="compact ? dialogActivator.attrs : {}"
v-on="compact ? dialogActivator.on : {}"
/>
<person-chip
v-for="teacher in amendedTeachers"
:key="documentation.id + '-amendedTeacher-' + teacher.id"
:person="teacher"
no-link
v-bind="compact ? dialogActivator.attrs : {}"
v-on="compact ? dialogActivator.on : {}"
class="text-decoration-line-through"
disabled
/>
</div>
</div>
</template>
<script>
import SubjectChip from "aleksis.apps.cursus/components/SubjectChip.vue";
import { DateTime } from "luxon";
import documentationPartMixin from "./documentationPartMixin";
export default {
name: "LessonInformation",
mixins: [documentationPartMixin],
components: {
SubjectChip,
},
methods: {
toDateTime(dateString) {
return DateTime.fromISO(dateString);
},
},
computed: {
largeGrid() {
return this.compact && !this.$vuetify.breakpoint.mobile;
},
amendedTeachers() {
if (
this.documentation?.amends?.amends?.teachers &&
this.documentation.amends.amends.teachers.length
) {
return this.documentation.amends.amends.teachers.filter(
(at) => !this.documentation.teachers.includes((t) => t.id === at.id),
);
}
return [];
},
},
};
</script>
<style scoped>
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
align-items: center;
gap: 1em;
align-content: start;
}
.large-grid {
grid-template-columns: 1fr 1fr 1fr 1fr;
align-content: unset;
}
.grid:last-child {
justify-self: end;
justify-content: end;
}
.fit-content {
width: fit-content;
}
.gap {
gap: 0.25em;
}
</style>
<template>
<div
class="d-flex align-center justify-space-between justify-md-end flex-wrap gap"
>
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
<v-chip dense color="success">
<v-chip small dense class="mr-2" color="green darken-3 white--text"
>26</v-chip
>
von 30 anwesend
</v-chip>
<v-chip dense color="warning">
<v-chip small dense class="mr-2" color="orange darken-3 white--text"
>3</v-chip
>
entschuldigt
</v-chip>
<v-chip dense color="error">
<v-chip small dense class="mr-2" color="red darken-3 white--text"
>1</v-chip
>
unentschuldigt
</v-chip>
<v-chip dense color="grey lighten-1">
<v-chip small dense class="mr-2" color="grey darken-1 white--text"
>4</v-chip
>
Hausaufgaben vergessen
</v-chip>
<v-chip dense color="primary" outlined>
<v-icon>$edit</v-icon>
</v-chip>
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
</div>
</template>
<script>
import documentationPartMixin from "./documentationPartMixin";
export default {
name: "LessonNotes",
mixins: [documentationPartMixin],
};
</script>
<style scoped>
.gap {
gap: 0.25em;
}
</style>
<template>
<div>
<!-- compact -->
<div
class="d-flex flex-column flex-md-row align-stretch align-md-center gap justify-start fill-height"
v-if="compact"
>
<documentation-compact-details
v-bind="dialogActivator.attrs"
v-on="dialogActivator.on"
v-if="
!documentation.canEdit &&
(documentation.topic ||
documentation.homework ||
documentation.groupNote)
"
:documentation="documentation"
@click="$emit('open')"
:class="{
'flex-grow-1 min-width pa-1 read-only-grid': true,
'full-width': $vuetify.breakpoint.mobile,
}"
/>
<v-alert
v-else-if="documentation.futureNotice"
type="warning"
outlined
class="min-width flex-grow-1 mb-0"
>
{{ $t("alsijil.coursebook.notices.future") }}
</v-alert>
<v-alert
v-else-if="!documentation.canEdit"
type="info"
outlined
class="min-width flex-grow-1 mb-0"
>
{{ $t("alsijil.coursebook.notices.no_entry") }}
</v-alert>
<v-text-field
v-if="documentation.canEdit"
:class="{
'flex-grow-1 min-width': true,
'full-width': $vuetify.breakpoint.mobile,
}"
hide-details
outlined
:label="$t('alsijil.coursebook.summary.topic.label')"
:value="documentation.topic"
@input="topic = $event"
@focusout="save"
@keydown.enter="saveAndBlur"
:loading="loading"
>
<template #append>
<v-tooltip bottom>
<template #activator="{ on, attrs }">
<v-scroll-x-transition>
<v-icon
v-if="appendIcon"
:color="appendIconColor"
v-on="on"
v-bind="attrs"
>{{ appendIcon }}</v-icon
>
</v-scroll-x-transition>
</template>
<span>{{ appendIconTooltip }}</span>
</v-tooltip>
</template>
</v-text-field>
<div
:class="{
'flex-grow-1 max-width': true,
'full-width': $vuetify.breakpoint.mobile,
}"
v-if="documentation.canEdit"
>
<v-card
v-bind="dialogActivator.attrs"
v-on="dialogActivator.on"
outlined
@click="$emit('open')"
class="max-width grid-layout pa-1"
dense
rounded="lg"
>
<span class="max-width text-truncate">{{
documentation.homework
? $t("alsijil.coursebook.summary.homework.value", documentation)
: $t("alsijil.coursebook.summary.homework.empty")
}}</span>
<v-icon right class="float-right">{{ homeworkIcon }}</v-icon>
<span class="max-width text-truncate">{{
documentation.groupNote
? $t("alsijil.coursebook.summary.group_note.value", documentation)
: $t("alsijil.coursebook.summary.group_note.empty")
}}</span>
<v-icon right class="float-right">{{ groupNoteIcon }}</v-icon>
</v-card>
</div>
</div>
<!-- not compact -->
<!-- Are focusout & enter enough trigger? -->
<v-text-field
filled
v-if="!compact && documentation.canEdit"
:label="$t('alsijil.coursebook.summary.topic.label')"
:value="documentation.topic"
@input="topic = $event"
/>
<v-textarea
filled
auto-grow
rows="3"
clearable
v-if="!compact && documentation.canEdit"
:label="$t('alsijil.coursebook.summary.homework.label')"
:value="documentation.homework"
@input="homework = $event ? $event : ''"
/>
<v-textarea
filled
auto-grow
rows="3"
clearable
v-if="!compact && documentation.canEdit"
:label="$t('alsijil.coursebook.summary.group_note.label')"
:value="documentation.groupNote"
@input="groupNote = $event ? $event : ''"
/>
<documentation-full-details
v-if="!compact && !documentation.canEdit"
:documentation="documentation"
/>
</div>
</template>
<script setup>
import DocumentationCompactDetails from "./DocumentationCompactDetails.vue";
import DocumentationFullDetails from "./DocumentationFullDetails.vue";
</script>
<script>
import createOrPatchMixin from "aleksis.core/mixins/createOrPatchMixin.js";
import documentationPartMixin from "./documentationPartMixin";
export default {
name: "LessonSummary",
mixins: [createOrPatchMixin, documentationPartMixin],
emits: ["open"],
data() {
return {
topic: null,
homework: null,
groupNote: null,
appendIcon: null,
topicError: null,
};
},
methods: {
handleUpdateAfterCreateOrPatch(itemId) {
return (cached, incoming) => {
for (const object of incoming) {
console.log("summary: handleUpdateAfterCreateOrPatch", object);
// Replace the current documentation
const index = cached.findIndex(
(o) => o[itemId] === this.documentation.id,
);
// merged with the incoming partial documentation
// if creation of proper documentation from dummy one, set ID of documentation currently being edited as oldID so that key in coursebook doesn't change
cached[index] = {
...this.documentation,
...object,
oldId:
this.documentation.id !== object.id
? this.documentation.id
: this.documentation.oldId,
};
}
return cached;
};
},
handleAppendIconSuccess() {
this.topicError = null;
this.appendIcon = "$success";
setTimeout(() => {
this.appendIcon = "";
}, 3000);
},
save() {
if (
this.topic !== null ||
this.homework !== null ||
this.groupNote !== null
) {
this.createOrPatch([
{
id: this.documentation.id,
...(this.topic !== null && { topic: this.topic }),
...(this.homework !== null && { homework: this.homework }),
...(this.groupNote !== null && { groupNote: this.groupNote }),
},
]);
this.topic = null;
this.homework = null;
this.groupNote = null;
}
},
saveAndBlur(event) {
this.save();
event.target.blur();
},
handleError(error) {
this.appendIcon = "$error";
this.topicError = error;
},
},
computed: {
homeworkIcon() {
if (this.documentation.homework) {
return this.documentation.canEdit
? "mdi-book-edit-outline"
: "mdi-book-alert-outline";
}
return this.documentation.canEdit
? "mdi-book-plus-outline"
: "mdi-book-off-outline";
},
groupNoteIcon() {
if (this.documentation.groupNote) {
return this.documentation.canEdit
? "mdi-note-edit-outline"
: "mdi-note-alert-outline";
}
return this.documentation.canEdit
? "mdi-note-plus-outline"
: "mdi-note-off-outline";
},
minWidth() {
return Math.min(this.documentation?.topic?.length || 15, 15) + "ch";
},
maxWidth() {
return this.$vuetify.breakpoint.mobile ? "100%" : "20ch";
},
appendIconColor() {
return (
{ $success: "success", $error: "error" }[this.appendIcon] || "primary"
);
},
appendIconTooltip() {
return (
{
$success: this.$t("alsijil.coursebook.summary.topic.status.success"),
$error: this.$t("alsijil.coursebook.summary.topic.status.error", {
error: this.topicError,
}),
}[this.appendIcon] || ""
);
},
},
mounted() {
this.$on("save", this.handleAppendIconSuccess);
},
};
</script>
<style scoped>
.min-width {
min-width: v-bind(minWidth);
}
.max-width {
max-width: v-bind(maxWidth);
}
.gap {
gap: 1em;
}
.grid-layout {
display: grid;
grid-template-columns: auto min-content;
}
.read-only-grid {
display: grid;
grid-template-columns: min-content auto;
grid-template-rows: auto;
}
</style>
/**
* Mixin to provide common fields for all components specific to a singular documentation inside the coursebook
*/
export default {
props: {
/**
* The documentation in question
*/
documentation: {
type: Object,
required: true,
},
/**
* Whether the documentation is currently in the compact mode (meaning coursebook row)
*/
compact: {
type: Boolean,
required: false,
default: false,
},
/**
* Activator attributes and event listeners to open documentation dialog in different places
*/
dialogActivator: {
type: Object,
required: false,
default: () => ({ attrs: {}, on: {} }),
},
},
computed: {
/**
* All necessary props bundled together to easily pass to child components
* @returns {{compact: Boolean, documentation: Object, dialogActivator: Object<{attrs: Object, on: Object}>}}
*/
documentationPartProps() {
return {
documentation: this.documentation,
compact: this.compact,
dialogActivator: this.dialogActivator,
};
},
},
};
......@@ -2,206 +2,20 @@ import {
notLoggedInValidator,
hasPersonValidator,
} from "aleksis.core/routeValidators";
import { DateTime } from "luxon";
export default {
meta: {
inMenu: true,
titleKey: "alsijil.menu_title",
icon: "mdi-account-group-outline",
iconActive: "mdi-account-group",
validators: [hasPersonValidator],
},
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
children: [
{
path: "lesson",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.lessonPeriod",
meta: {
inMenu: true,
titleKey: "alsijil.lesson.menu_title",
icon: "mdi-alarm",
permission: "alsijil.view_lesson_menu_rule",
},
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "lesson/:year(\\d+)/:week(\\d+)/:id_(\\d+)",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.lessonPeriodByCWAndID",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "extra_lesson/:id_(\\d+)/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.extraLessonByID",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "event/:id_(\\d+)/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.eventByID",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "week/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.weekView",
meta: {
inMenu: true,
titleKey: "alsijil.week.menu_title",
icon: "mdi-view-week-outline",
permission: "alsijil.view_week_menu_rule",
},
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "week/:year(\\d+)/:week(\\d+)/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.weekViewByWeek",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "week/year/cw/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.weekViewPlaceholders",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "week/:type_/:id_(\\d+)/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.weekViewByTypeAndID",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "week/year/cw/:type_/:id_(\\d+)/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.weekViewPlaceholdersByTypeAndID",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "week/:year(\\d+)/:week(\\d+)/:type_/:id_(\\d+)/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.weekViewByWeekTypeAndID",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "print/group/:id_(\\d+)",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.fullRegisterGroup",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "groups/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.myGroups",
meta: {
inMenu: true,
titleKey: "alsijil.groups.menu_title",
icon: "mdi-account-multiple-outline",
permission: "alsijil.view_my_groups_rule",
},
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "groups/:pk(\\d+)/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.studentsList",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "persons/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.myStudents",
meta: {
inMenu: true,
titleKey: "alsijil.persons.menu_title",
icon: "mdi-account-school-outline",
permission: "alsijil.view_my_students_rule",
},
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "persons/:id_(\\d+)/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.overviewPerson",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "me/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.overviewMe",
meta: {
inMenu: true,
titleKey: "alsijil.my_overview.menu_title",
icon: "mdi-chart-box-outline",
permission: "alsijil.view_person_overview_menu_rule",
},
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "notes/:pk(\\d+)/delete/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.deletePersonalNote",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "absence/new/:id_(\\d+)/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.registerAbsenceWithID",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "absence/new/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.registerAbsence",
meta: {
inMenu: true,
titleKey: "alsijil.absence.menu_title",
icon: "mdi-message-alert-outline",
permission: "alsijil.view_register_absence_rule",
},
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "extra_marks/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
......@@ -210,6 +24,7 @@ export default {
inMenu: true,
titleKey: "alsijil.extra_marks.menu_title",
icon: "mdi-label-variant-outline",
iconActive: "mdi-label-variant",
permission: "alsijil.view_extramarks_rule",
},
props: {
......@@ -241,156 +56,40 @@ export default {
},
},
{
path: "excuse_types/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.excuseTypes",
meta: {
inMenu: true,
titleKey: "alsijil.excuse_types.menu_title",
icon: "mdi-label-outline",
permission: "alsijil.view_excusetypes_rule",
},
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "excuse_types/create/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.createExcuseType",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
path: "coursebook/",
component: () => import("./components/coursebook/Coursebook.vue"),
redirect: () => {
return {
name: "alsijil.coursebook",
params: {
filterType: "my",
},
hash: "#" + DateTime.now().toISODate(),
};
},
},
{
path: "excuse_types/:pk(\\d+)/edit/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.editExcuseType",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "excuse_types/:pk(\\d+)/delete/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.deleteExcuseType",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "group_roles/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.groupRoles",
name: "alsijil.coursebook_landing",
props: true,
meta: {
inMenu: true,
titleKey: "alsijil.group_roles.menu_title_manage",
icon: "mdi-clipboard-plus-outline",
permission: "alsijil.view_grouproles_rule",
},
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "group_roles/create/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.createGroupRole",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "group_roles/:pk(\\d+)/edit/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.editGroupRole",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "group_roles/:pk(\\d+)/delete/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.deleteGroupRole",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "groups/:pk(\\d+)/group_roles/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.assignedGroupRoles",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "groups/:pk(\\d+)/group_roles/assign/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.assignGroupRole",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "groups/:pk(\\d+)/group_roles/:role_pk(\\d+)/assign/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.assignGroupRoleByRolePK",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "group_roles/assignments/:pk(\\d+)/edit/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.editGroupRoleAssignment",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "group_roles/assignments/:pk(\\d+)/stop/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.stopGroupRoleAssignment",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "group_roles/assignments/:pk(\\d+)/delete/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.deleteGroupRoleAssignment",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "group_roles/assignments/assign/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.assignGroupRoleMultiple",
meta: {
inMenu: true,
titleKey: "alsijil.group_roles.menu_title_assign",
icon: "mdi-clipboard-account-outline",
permission: "alsijil.assign_grouprole_for_multiple_rule",
},
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{
path: "all/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.allRegisterObjects",
meta: {
inMenu: true,
titleKey: "alsijil.all_lessons.menu_title",
icon: "mdi-format-list-text",
permission: "alsijil.view_register_objects_list_rule",
},
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: 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",
},
children: [
{
path: ":filterType(my|all)/:objType(group|course|teacher)?/:objId(\\d+)?/",
component: () => import("./components/coursebook/Coursebook.vue"),
name: "alsijil.coursebook",
meta: {
titleKey: "alsijil.coursebook.menu_title",
toolbarTitle: "alsijil.coursebook.menu_title",
permission: "alsijil.view_documentations_menu_rule",
fullWidth: true,
},
},
],
},
],
};
......@@ -31,6 +31,47 @@
},
"all_lessons": {
"menu_title": "Alle Stunden"
},
"coursebook": {
"menu_title": "Kursbuch",
"page_title": "Kursbuch für {name}",
"title_plural": "Kursbuch",
"status": {
"available": "Kursbucheintrag vorhanden",
"missing": "Kursbucheintrag fehlt",
"running": "Stunde läuft",
"substitution": "Vertretungsstunde",
"cancelled": "Stunde fällt aus",
"pending": "Stunde in der Zukunft"
},
"summary": {
"topic": {
"label": "Topic",
"status": {
"success": "Topic saved successfully",
"error": "There has been an error while saving the topic: {error}"
}
},
"homework": {
"label": "Hausaufgaben",
"value": "HA: {homework}",
"empty": "Keine Hausaufgaben"
},
"group_note": {
"label": "Gruppennotiz",
"value": "GN: {groupNote}",
"empty": "Keine Gruppennotiz"
}
},
"filter": {
"own": "Nur eigene Stunden anzeigen",
"missing": "Nur unvollständige Stunden anzeigen",
"groups": "Gruppen",
"courses": "Kurse",
"filter_for_obj": "Nach Gruppe und Kurs filtern"
},
"no_data": "Keine Stunden der ausgewählten Gruppen und Kurse im aktuellen Zeitraum",
"no_results": "Keine Suchergebnisse für {search}"
}
}
}
......@@ -31,6 +31,51 @@
"all_lessons": {
"menu_title": "All lessons"
},
"menu_title": "Class register"
"menu_title": "Class register",
"coursebook": {
"menu_title": "Coursebook",
"page_title": "Coursebook for {name}",
"title_plural": "Coursebook",
"status": {
"available": "Documentation available",
"missing": "Documentation missing",
"running": "Lesson running",
"substitution": "Substitution lesson",
"cancelled": "Lesson cancelled",
"pending": "Lesson pending"
},
"summary": {
"topic": {
"label": "Topic",
"status": {
"success": "Topic saved successfully",
"error": "There has been an error while saving the topic: {error}"
}
},
"homework": {
"label": "Homework",
"value": "HW: {homework}",
"empty": "No homework"
},
"group_note": {
"label": "Group note",
"value": "GN: {groupNote}",
"empty": "No group note"
}
},
"notices": {
"future": "Editing this lesson isn't allowed as this lesson is in the future.",
"no_entry": "There is no entry for this lesson yet."
},
"filter": {
"own": "Only show own lessons",
"missing": "Only show incomplete lessons",
"groups": "Groups",
"courses": "Courses",
"filter_for_obj": "Filter for group and course"
},
"no_data": "No lessons for the selected groups and courses in this period",
"no_results": "No search results for {search}"
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment