✨ Implement timetable editing
This commit is contained in:
23
src/App.vue
23
src/App.vue
@ -41,7 +41,7 @@ const isDataView = computed(() => route.meta.dataView || false);
|
|||||||
:error="loadingFailed"
|
:error="loadingFailed"
|
||||||
/>
|
/>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<main>
|
<div class="container">
|
||||||
<DateSelector
|
<DateSelector
|
||||||
:selectedDate="selectedDate"
|
:selectedDate="selectedDate"
|
||||||
:selectedDay="selectedDay"
|
:selectedDay="selectedDay"
|
||||||
@ -49,10 +49,12 @@ const isDataView = computed(() => route.meta.dataView || false);
|
|||||||
@changeDate="(date) => (changeDate = date)"
|
@changeDate="(date) => (changeDate = date)"
|
||||||
v-show="isDataView"
|
v-show="isDataView"
|
||||||
/>
|
/>
|
||||||
<div class="wrapper">
|
<main>
|
||||||
<RouterView />
|
<div class="wrapper">
|
||||||
</div>
|
<RouterView />
|
||||||
</main>
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
<BottomNavbar v-show="!$route.meta.hideNav" />
|
<BottomNavbar v-show="!$route.meta.hideNav" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -90,15 +92,20 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
width: 100%;
|
height: 100%;
|
||||||
max-width: 900px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: 1fr;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
width: inherit;
|
||||||
|
max-width: 900px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,25 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { getSubstitutionColor } from "@/util";
|
import { getSubstitutionColor } from "@/util";
|
||||||
import { times } from "@/store";
|
import { times } from "@/store";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { TrashIcon } from "lucide-vue-next";
|
||||||
|
|
||||||
defineProps(["lesson", "index"]);
|
const props = defineProps(["lesson", "index", "edit", "buttons"]);
|
||||||
|
const emit = defineEmits(["change", "delete"]);
|
||||||
|
|
||||||
|
const subjectElement = ref();
|
||||||
|
const teacherElement = ref();
|
||||||
|
const roomElement = ref();
|
||||||
|
const lessonLength = ref(props.lesson.length || 1);
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
emit("change", {
|
||||||
|
subject: subjectElement.value.innerText,
|
||||||
|
teacher: teacherElement.value.innerText,
|
||||||
|
room: roomElement.value.innerText,
|
||||||
|
length: lessonLength.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function isChanged(lesson, key) {
|
function isChanged(lesson, key) {
|
||||||
const substitution = lesson.substitution;
|
const substitution = lesson.substitution;
|
||||||
@ -43,13 +60,21 @@ function getTime(index) {
|
|||||||
<div class="infos">
|
<div class="infos">
|
||||||
<!-- Subject changed -->
|
<!-- Subject changed -->
|
||||||
<span class="subject" v-if="isChanged(lesson, 'subject')">
|
<span class="subject" v-if="isChanged(lesson, 'subject')">
|
||||||
<s>{{ lesson.subject }}</s> {{ lesson.substitution.change.subject }}
|
<s>{{ lesson.subject }}</s>
|
||||||
|
{{ lesson.substitution.change.subject }}
|
||||||
</span>
|
</span>
|
||||||
<!-- Cancellation -->
|
<!-- Cancellation -->
|
||||||
<span class="subject" v-else-if="isCancelled(lesson.substitution)">
|
<span class="subject" v-else-if="isCancelled(lesson.substitution)">
|
||||||
<s>{{ lesson.subject }}</s>
|
<s>{{ lesson.subject }}</s>
|
||||||
</span>
|
</span>
|
||||||
<span class="subject" v-else>
|
<span
|
||||||
|
class="subject"
|
||||||
|
v-else
|
||||||
|
:contenteditable="edit"
|
||||||
|
data-ph="Subject"
|
||||||
|
ref="subjectElement"
|
||||||
|
@input="update"
|
||||||
|
>
|
||||||
{{ lesson.subject }}
|
{{ lesson.subject }}
|
||||||
</span>
|
</span>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
@ -58,22 +83,43 @@ function getTime(index) {
|
|||||||
<s>{{ lesson.teacher }}</s>
|
<s>{{ lesson.teacher }}</s>
|
||||||
{{ lesson.substitution.change.teacher }}</span
|
{{ lesson.substitution.change.teacher }}</span
|
||||||
>
|
>
|
||||||
<span class="info" v-else> {{ lesson.teacher }}</span>
|
<span
|
||||||
|
class="info"
|
||||||
|
v-else
|
||||||
|
:contenteditable="edit"
|
||||||
|
data-ph="Teacher"
|
||||||
|
ref="teacherElement"
|
||||||
|
@input="update"
|
||||||
|
>
|
||||||
|
{{ lesson.teacher }}</span
|
||||||
|
>
|
||||||
<!-- Room changed -->
|
<!-- Room changed -->
|
||||||
<span class="info" v-if="isChanged(lesson, 'room')"
|
<span class="info" v-if="isChanged(lesson, 'room')"
|
||||||
>, <s>{{ lesson.room }}</s> {{ lesson.substitution.change.room }}
|
>, <s>{{ lesson.room }}</s> {{ lesson.substitution.change.room }}
|
||||||
</span>
|
</span>
|
||||||
<span class="info" v-else-if="lesson.room">, {{ lesson.room }}</span>
|
<span class="info" v-else-if="lesson.room || edit"
|
||||||
|
>,
|
||||||
|
<span
|
||||||
|
:contenteditable="edit"
|
||||||
|
data-ph="Room"
|
||||||
|
ref="roomElement"
|
||||||
|
@input="update"
|
||||||
|
>{{ lesson.room }}</span
|
||||||
|
></span
|
||||||
|
>
|
||||||
<!-- Show notes if available -->
|
<!-- Show notes if available -->
|
||||||
<span class="info" v-if="getNotes(lesson.substitution)"
|
<span class="info" v-if="getNotes(lesson.substitution)"
|
||||||
>, {{ $t("timetable.notes") }} {{ getNotes(lesson.substitution) }}
|
>, {{ $t("timetable.notes") }} {{ getNotes(lesson.substitution) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="times" v-if="getTime(index).start">
|
<div class="times" v-if="getTime(index).start && !edit">
|
||||||
<span>{{ getTime(index).start }} -</span>
|
<span>{{ getTime(index).start }} -</span>
|
||||||
<span>{{ getTime(index).end }}</span>
|
<span>{{ getTime(index).end }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="buttons" v-if="edit && buttons">
|
||||||
|
<TrashIcon @click="$emit('delete')" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -119,4 +165,19 @@ function getTime(index) {
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
margin-left: auto;
|
||||||
|
opacity: 0.7;
|
||||||
|
display: grid;
|
||||||
|
gap: 5px;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
[contenteditable="true"]:empty:before {
|
||||||
|
content: attr(data-ph);
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
100
src/components/lesson-group-list.vue
Normal file
100
src/components/lesson-group-list.vue
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<script setup>
|
||||||
|
import { toRaw, ref } from "vue";
|
||||||
|
import LessonCard from "@/components/lesson-card.vue";
|
||||||
|
import {
|
||||||
|
ArrowUpIcon,
|
||||||
|
ArrowDownIcon,
|
||||||
|
SettingsIcon,
|
||||||
|
TrashIcon,
|
||||||
|
PlusIcon,
|
||||||
|
} from "lucide-vue-next";
|
||||||
|
|
||||||
|
const props = defineProps(["lesson", "index", "edit"]);
|
||||||
|
const emit = defineEmits(["change", "move"]);
|
||||||
|
|
||||||
|
const advancedOptions = ref(false);
|
||||||
|
|
||||||
|
const lessonLength = ref(1);
|
||||||
|
function updateLength() {
|
||||||
|
let newLesson = structuredClone(toRaw(props.lesson));
|
||||||
|
for (let entry of newLesson) {
|
||||||
|
entry.length = lessonLength.value;
|
||||||
|
}
|
||||||
|
emit("change", newLesson);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendOption() {
|
||||||
|
let newLesson = structuredClone(toRaw(props.lesson));
|
||||||
|
if (Array.isArray(newLesson)) newLesson.push({});
|
||||||
|
else newLesson = [newLesson, {}];
|
||||||
|
emit("change", newLesson);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lessonChanged(index, data) {
|
||||||
|
let newLesson = structuredClone(toRaw(props.lesson));
|
||||||
|
newLesson[index] = data;
|
||||||
|
if (newLesson.length > 1)
|
||||||
|
newLesson[index].group = `${data.subject}: ${data.teacher}`;
|
||||||
|
else delete newLesson[index].group;
|
||||||
|
emit("change", newLesson);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lessonDeleted(index) {
|
||||||
|
let newLesson = structuredClone(toRaw(props.lesson));
|
||||||
|
newLesson.splice(index, 1);
|
||||||
|
if (newLesson.length == 0) emit("delete");
|
||||||
|
else emit("change", newLesson);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="lessonList">
|
||||||
|
<div class="buttons" v-if="edit">
|
||||||
|
<ArrowUpIcon @click="$emit('move', 'up')" />
|
||||||
|
<ArrowDownIcon @click="$emit('move', 'down')" />
|
||||||
|
<SettingsIcon @click="advancedOptions = !advancedOptions" />
|
||||||
|
<TrashIcon @click="$emit('delete')" />
|
||||||
|
<PlusIcon @click="appendOption" />
|
||||||
|
</div>
|
||||||
|
<LessonCard
|
||||||
|
v-for="(option, groupIndex) in lesson"
|
||||||
|
:key="groupIndex"
|
||||||
|
:lesson="option"
|
||||||
|
:index="index"
|
||||||
|
:edit="edit"
|
||||||
|
:buttons="edit"
|
||||||
|
@change="(data) => lessonChanged(groupIndex, data)"
|
||||||
|
@delete="() => lessonDeleted(groupIndex)"
|
||||||
|
/>
|
||||||
|
<div class="advanced" v-show="advancedOptions" v-if="edit">
|
||||||
|
<span>Length: </span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
v-model="lessonLength"
|
||||||
|
@change="updateLength"
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.lessonList {
|
||||||
|
border-left: 2px solid var(--text-color);
|
||||||
|
padding-left: 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
|
</style>
|
44
src/components/settings/editor-navbar.vue
Normal file
44
src/components/settings/editor-navbar.vue
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<script setup>
|
||||||
|
import { BanIcon, SaveIcon } from "lucide-vue-next";
|
||||||
|
|
||||||
|
defineEmits(["abort", "save"]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="bottomnav">
|
||||||
|
<div class="entry danger" @click="$emit('back')">
|
||||||
|
<BanIcon />
|
||||||
|
<span>Abort</span>
|
||||||
|
</div>
|
||||||
|
<div class="entry success" @click="$emit('save')">
|
||||||
|
<SaveIcon />
|
||||||
|
<span>Save</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.bottomnav {
|
||||||
|
width: calc(90% - 40px);
|
||||||
|
height: 50px;
|
||||||
|
max-width: 800px;
|
||||||
|
padding: 0px 20px;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-columns: 1fr;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
z-index: 100;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: var(--bottomnav-color);
|
||||||
|
box-shadow: var(--bottomnav-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
@ -42,7 +42,7 @@ const linkedTimetable = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="listContainer">
|
<div class="listContainer">
|
||||||
<template v-for="(lesson, index) in linkedTimetable" :key="index">
|
<template v-for="(lesson, index) in linkedTimetable" :key="index">
|
||||||
<LessonCard :lesson="lesson" :index="index" />
|
<LessonCard :lesson="lesson" :index="index" :edit="false" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -9,6 +9,7 @@ import LoginView from "@/views/LoginView.vue";
|
|||||||
import TokenView from "@/views/TokenView.vue";
|
import TokenView from "@/views/TokenView.vue";
|
||||||
import FilteringSettings from "@/views/settings/FilteringSettings.vue";
|
import FilteringSettings from "@/views/settings/FilteringSettings.vue";
|
||||||
import TimetableSettings from "@/views/settings/TimetableSettings.vue";
|
import TimetableSettings from "@/views/settings/TimetableSettings.vue";
|
||||||
|
import TimetableEditor from "@/views/settings/TimetableEditor.vue";
|
||||||
import TimetableGroupSettings from "@/views/settings/TimetableGroupSettings.vue";
|
import TimetableGroupSettings from "@/views/settings/TimetableGroupSettings.vue";
|
||||||
import AppearanceSettings from "@/views/settings/AppearanceSettings.vue";
|
import AppearanceSettings from "@/views/settings/AppearanceSettings.vue";
|
||||||
import AboutPage from "@/views/settings/AboutPage.vue";
|
import AboutPage from "@/views/settings/AboutPage.vue";
|
||||||
@ -76,6 +77,14 @@ const router = createRouter({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/settings/timetable/edit/:id?",
|
||||||
|
name: "title.editTimetable",
|
||||||
|
component: TimetableEditor,
|
||||||
|
meta: {
|
||||||
|
hideNav: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/login",
|
path: "/login",
|
||||||
name: "title.login",
|
name: "title.login",
|
||||||
|
16
src/store.js
16
src/store.js
@ -34,9 +34,13 @@ watch(
|
|||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
);
|
);
|
||||||
watch(localTimetables, (newValue) => {
|
watch(
|
||||||
localStorage.setItem("timetables", JSON.stringify(newValue));
|
localTimetables,
|
||||||
});
|
(newValue) => {
|
||||||
|
localStorage.setItem("timetables", JSON.stringify(newValue));
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
watch(theme, (newValue) => {
|
watch(theme, (newValue) => {
|
||||||
localStorage.setItem("theme", newValue);
|
localStorage.setItem("theme", newValue);
|
||||||
});
|
});
|
||||||
@ -186,7 +190,7 @@ export const parsedTimetable = computed(() => {
|
|||||||
let usedLesson = { ...lesson };
|
let usedLesson = { ...lesson };
|
||||||
// Check if lesson has multiple options
|
// Check if lesson has multiple options
|
||||||
// (timetable groups)
|
// (timetable groups)
|
||||||
if (Array.isArray(lesson)) {
|
if (Array.isArray(lesson) && lesson.length > 1) {
|
||||||
let matchingLesson = lesson.find((e) =>
|
let matchingLesson = lesson.find((e) =>
|
||||||
timetableGroups.value.includes(e.group)
|
timetableGroups.value.includes(e.group)
|
||||||
);
|
);
|
||||||
@ -200,6 +204,8 @@ export const parsedTimetable = computed(() => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
usedLesson = { ...matchingLesson };
|
usedLesson = { ...matchingLesson };
|
||||||
|
} else if (Array.isArray(lesson)) {
|
||||||
|
usedLesson = { ...lesson[0] };
|
||||||
}
|
}
|
||||||
// Duplicate the lesson if its length is > 1 for it
|
// Duplicate the lesson if its length is > 1 for it
|
||||||
// to show up multiple times in the timetable view
|
// to show up multiple times in the timetable view
|
||||||
@ -220,7 +226,7 @@ export const possibleTimetableGroups = computed(() => {
|
|||||||
for (const lesson of day) {
|
for (const lesson of day) {
|
||||||
if (Array.isArray(lesson)) {
|
if (Array.isArray(lesson)) {
|
||||||
for (const group of lesson) {
|
for (const group of lesson) {
|
||||||
if (!foundTimetableGroups.includes(group.group)) {
|
if (group.group && !foundTimetableGroups.includes(group.group)) {
|
||||||
foundTimetableGroups.push(group.group);
|
foundTimetableGroups.push(group.group);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ export const strings = {
|
|||||||
history: "History",
|
history: "History",
|
||||||
login: "Login",
|
login: "Login",
|
||||||
token: "Token",
|
token: "Token",
|
||||||
|
editTimetable: "Edit Timetable",
|
||||||
settings: {
|
settings: {
|
||||||
main: "Settings",
|
main: "Settings",
|
||||||
filtering: "Filtering",
|
filtering: "Filtering",
|
||||||
@ -28,6 +29,7 @@ export const strings = {
|
|||||||
text: {
|
text: {
|
||||||
filtering:
|
filtering:
|
||||||
"Select a class here so the correct timetable is used and only relevant substitutions are shown.",
|
"Select a class here so the correct timetable is used and only relevant substitutions are shown.",
|
||||||
|
createTimetable: "Create a new Timetable",
|
||||||
timetableGroups:
|
timetableGroups:
|
||||||
"A timetable group defines, which lesson should be displayed, and which substitution should be shown if there are multiple possibilities for a single lesson within one class.",
|
"A timetable group defines, which lesson should be displayed, and which substitution should be shown if there are multiple possibilities for a single lesson within one class.",
|
||||||
language: "Change the language of all texts in the application.",
|
language: "Change the language of all texts in the application.",
|
||||||
@ -110,6 +112,9 @@ export const strings = {
|
|||||||
token: {
|
token: {
|
||||||
header: "Generate API-Token",
|
header: "Generate API-Token",
|
||||||
},
|
},
|
||||||
|
editor: {
|
||||||
|
newLesson: "Create new Lesson",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
de: {
|
de: {
|
||||||
title: {
|
title: {
|
||||||
@ -117,6 +122,7 @@ export const strings = {
|
|||||||
substitutions: "Vertretungsplan",
|
substitutions: "Vertretungsplan",
|
||||||
history: "Verlauf",
|
history: "Verlauf",
|
||||||
login: "Anmelden",
|
login: "Anmelden",
|
||||||
|
editTimetable: "Stundenplan Editieren",
|
||||||
settings: {
|
settings: {
|
||||||
main: "Einstellungen",
|
main: "Einstellungen",
|
||||||
filtering: "Filter",
|
filtering: "Filter",
|
||||||
@ -139,6 +145,7 @@ export const strings = {
|
|||||||
text: {
|
text: {
|
||||||
filtering:
|
filtering:
|
||||||
"Wähle hier deine Klasse aus, damit du deinen Stundenplan angezeigt bekommst und du nur relevante Vertretungen siehst.",
|
"Wähle hier deine Klasse aus, damit du deinen Stundenplan angezeigt bekommst und du nur relevante Vertretungen siehst.",
|
||||||
|
createTimetable: "Neuen Stundenplan erstellen",
|
||||||
timetableGroups:
|
timetableGroups:
|
||||||
"Stundenplan-Gruppen legen fest, welche Stundenplan-Daten du angezeigt bekommst, wenn es mehrere Möglichkeiten für eine Stunde gibt.",
|
"Stundenplan-Gruppen legen fest, welche Stundenplan-Daten du angezeigt bekommst, wenn es mehrere Möglichkeiten für eine Stunde gibt.",
|
||||||
language: "Ändere die Sprache aller Texte dieser Anwendung.",
|
language: "Ändere die Sprache aller Texte dieser Anwendung.",
|
||||||
@ -220,5 +227,8 @@ export const strings = {
|
|||||||
"An den Vertretungen für diesen Tag wurde noch nichts geändert",
|
"An den Vertretungen für diesen Tag wurde noch nichts geändert",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
editor: {
|
||||||
|
newLesson: "Neue Stunde hinzufügen",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
221
src/views/settings/TimetableEditor.vue
Normal file
221
src/views/settings/TimetableEditor.vue
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||||
|
import { ref, toRaw } from "vue";
|
||||||
|
import { localTimetables } from "@/store";
|
||||||
|
import { PlusIcon } from "lucide-vue-next";
|
||||||
|
import EditorNavbar from "@/components/settings/editor-navbar.vue";
|
||||||
|
import DateSelector from "@/components/date-selector.vue";
|
||||||
|
import ScrollableContainer from "@/components/scrollable-container.vue";
|
||||||
|
import LessonGroupList from "@/components/lesson-group-list.vue";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const timetable = localTimetables.value.find(
|
||||||
|
(e) => e.id == route.params.id
|
||||||
|
) || {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
timetable.data = timetable.data.map((day) => {
|
||||||
|
if (!day) return [];
|
||||||
|
return day.map((lesson) => {
|
||||||
|
if (Array.isArray(lesson)) return toRaw(lesson);
|
||||||
|
else return [toRaw(lesson)];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const timetableClone = ref(structuredClone(toRaw(timetable)));
|
||||||
|
|
||||||
|
const swiperElement = ref();
|
||||||
|
function setSwiper(swiper) {
|
||||||
|
swiperElement.value = swiper;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLessonIndex(index, day) {
|
||||||
|
let lessonIndex = 0;
|
||||||
|
for (let i = 0; i < index; i++) {
|
||||||
|
lessonIndex += day[i][0].length || 1;
|
||||||
|
}
|
||||||
|
return lessonIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLessonLength(lesson) {
|
||||||
|
if (Array.isArray(lesson)) return lesson[0].length || 1;
|
||||||
|
else return lesson.length || 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayMove(arr, fromIndex, toIndex) {
|
||||||
|
let element = arr[fromIndex];
|
||||||
|
arr.splice(fromIndex, 1);
|
||||||
|
arr.splice(toIndex, 0, element);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="editor">
|
||||||
|
<DateSelector
|
||||||
|
:selectedDay="(swiperElement || { activeIndex: 0 }).activeIndex"
|
||||||
|
@changeDay="
|
||||||
|
(day) => swiperElement.slideTo(swiperElement.activeIndex + day)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<div class="settings">
|
||||||
|
<span>Name: </span>
|
||||||
|
<input type="text" v-model="timetableClone.title" />
|
||||||
|
</div>
|
||||||
|
<div class="wrapper">
|
||||||
|
<swiper
|
||||||
|
:slides-per-view="1"
|
||||||
|
:space-between="50"
|
||||||
|
:initial-slide="0"
|
||||||
|
@swiper="setSwiper"
|
||||||
|
>
|
||||||
|
<swiper-slide :key="day" v-for="day in 5">
|
||||||
|
<ScrollableContainer>
|
||||||
|
<div class="list">
|
||||||
|
<div
|
||||||
|
class="lesson"
|
||||||
|
v-for="(lesson, index) in timetableClone.data[day - 1]"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<!-- Add main editable element -->
|
||||||
|
<LessonGroupList
|
||||||
|
:index="getLessonIndex(index, timetableClone.data[day - 1])"
|
||||||
|
:lesson="lesson"
|
||||||
|
:edit="true"
|
||||||
|
@change="(e) => (timetableClone.data[day - 1][index] = e)"
|
||||||
|
@delete="timetableClone.data[day - 1].splice(index, 1)"
|
||||||
|
@move="
|
||||||
|
(direction) => {
|
||||||
|
if (direction == 'up' && index > 0) {
|
||||||
|
arrayMove(
|
||||||
|
timetableClone.data[day - 1],
|
||||||
|
index,
|
||||||
|
index - 1
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
direction == 'down' &&
|
||||||
|
index < timetableClone.data[day - 1].length
|
||||||
|
) {
|
||||||
|
arrayMove(
|
||||||
|
timetableClone.data[day - 1],
|
||||||
|
index,
|
||||||
|
index + 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<!-- Add placeholder elements for lessons with length > 1 -->
|
||||||
|
<div
|
||||||
|
class="inactive"
|
||||||
|
v-for="placeholderLesson in getLessonLength(lesson) - 1"
|
||||||
|
:key="placeholderLesson"
|
||||||
|
>
|
||||||
|
<LessonGroupList
|
||||||
|
:index="
|
||||||
|
getLessonIndex(index, timetableClone.data[day - 1]) +
|
||||||
|
placeholderLesson
|
||||||
|
"
|
||||||
|
:lesson="lesson"
|
||||||
|
:edit="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="create"
|
||||||
|
@click="
|
||||||
|
if (timetableClone.data[day - 1]) {
|
||||||
|
timetableClone.data[day - 1].push([{}]);
|
||||||
|
} else {
|
||||||
|
timetableClone.data[day - 1] = [[{}]];
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<PlusIcon />
|
||||||
|
<span>{{ $t("editor.newLesson") }}</span>
|
||||||
|
</div>
|
||||||
|
</ScrollableContainer>
|
||||||
|
</swiper-slide>
|
||||||
|
</swiper>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="navbar">
|
||||||
|
<EditorNavbar
|
||||||
|
@back="$router.back()"
|
||||||
|
@save="
|
||||||
|
() => {
|
||||||
|
timetable.data = timetableClone.data;
|
||||||
|
timetable.title = timetableClone.title;
|
||||||
|
timetable.source = 'Manual';
|
||||||
|
timetable.trusted = true;
|
||||||
|
$router.back();
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import "swiper/css";
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.editor {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings {
|
||||||
|
padding: 5px 10px 0px 10px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings input {
|
||||||
|
border: unset;
|
||||||
|
background-color: unset;
|
||||||
|
color: unset;
|
||||||
|
border-bottom: 1px solid var(--text-color);
|
||||||
|
outline: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper {
|
||||||
|
padding: 5px 10px 0px 10px;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inactive {
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px;
|
||||||
|
margin-top: 10px;
|
||||||
|
gap: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import TimetableCard from "@/components/settings/timetable-card.vue";
|
import TimetableCard from "@/components/settings/timetable-card.vue";
|
||||||
import { timetables, localTimetables, timetableId } from "@/store";
|
import { timetables, localTimetables, timetableId } from "@/store";
|
||||||
|
import { PlusIcon } from "lucide-vue-next";
|
||||||
import { toRaw } from "vue";
|
import { toRaw } from "vue";
|
||||||
|
|
||||||
function copyTimetable(timetable) {
|
function copyTimetable(timetable) {
|
||||||
@ -9,39 +10,54 @@ function copyTimetable(timetable) {
|
|||||||
newTimetable.id = new Date().getTime();
|
newTimetable.id = new Date().getTime();
|
||||||
localTimetables.value.push(newTimetable);
|
localTimetables.value.push(newTimetable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createTimetable() {
|
||||||
|
localTimetables.value.push({
|
||||||
|
id: new Date().getTime(),
|
||||||
|
title: "New timetable",
|
||||||
|
data: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h2>{{ $t("settings.heading.localTimetables") }}</h2>
|
<div class="content" v-if="$route.name == 'title.settings.timetable'">
|
||||||
<div class="list">
|
<h2>{{ $t("settings.heading.localTimetables") }}</h2>
|
||||||
<TimetableCard
|
<div class="list">
|
||||||
v-for="timetable in localTimetables"
|
<TimetableCard
|
||||||
:key="timetable.id"
|
v-for="timetable in localTimetables"
|
||||||
:timetable="timetable"
|
:key="timetable.id"
|
||||||
:selected="timetableId == timetable.id"
|
:timetable="timetable"
|
||||||
:editable="true"
|
:selected="timetableId == timetable.id"
|
||||||
@click="timetableId = timetable.id"
|
:editable="true"
|
||||||
@copy="copyTimetable(timetable)"
|
@click="timetableId = timetable.id"
|
||||||
@delete="
|
@edit="$router.push('timetable/edit/' + timetable.id)"
|
||||||
localTimetables.splice(
|
@copy="copyTimetable(timetable)"
|
||||||
localTimetables.findIndex((e) => e.id == timetable.id),
|
@delete="
|
||||||
1
|
localTimetables.splice(
|
||||||
)
|
localTimetables.findIndex((e) => e.id == timetable.id),
|
||||||
"
|
1
|
||||||
/>
|
)
|
||||||
</div>
|
"
|
||||||
<h2>{{ $t("settings.heading.remoteTimetables") }}</h2>
|
/>
|
||||||
<div class="list">
|
</div>
|
||||||
<TimetableCard
|
<div class="create" @click="createTimetable">
|
||||||
v-for="timetable in timetables"
|
<PlusIcon /> {{ $t("settings.text.createTimetable") }}
|
||||||
:key="timetable.id"
|
</div>
|
||||||
:timetable="timetable"
|
<h2>{{ $t("settings.heading.remoteTimetables") }}</h2>
|
||||||
:selected="timetableId == timetable.id"
|
<div class="list">
|
||||||
:editable="false"
|
<TimetableCard
|
||||||
@click="timetableId = timetable.id"
|
v-for="timetable in timetables"
|
||||||
@copy="copyTimetable(timetable)"
|
:key="timetable.id"
|
||||||
/>
|
:timetable="timetable"
|
||||||
|
:selected="timetableId == timetable.id"
|
||||||
|
:editable="false"
|
||||||
|
@click="timetableId = timetable.id"
|
||||||
|
@copy="copyTimetable(timetable)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<RouterView v-else />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -57,4 +73,13 @@ p {
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.create {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
Reference in New Issue
Block a user