Add support for multiple remote timetables

- Add timetable "title" column to db
- Make API return array of timetables
- Add settings page for selecting a timetable
- Add InfoCard if no timetable is selected
This commit is contained in:
2023-06-18 14:30:36 +02:00
parent dac0d09167
commit 48364d4c59
10 changed files with 176 additions and 30 deletions

View File

@ -13,7 +13,7 @@ export async function getTimetable(req, res) {
return;
}
const requestedClass = req.query.class.toLowerCase();
const timetable = await prisma.timetable.findFirst({
const timetables = await prisma.timetable.findMany({
where: {
class: requestedClass,
},
@ -22,19 +22,9 @@ export async function getTimetable(req, res) {
},
});
const times = await prisma.time.findMany();
if (!timetable) {
res.status(404).send({
success: false,
error: "no_timetable",
message: "No timetable was found for this class",
});
return;
}
res.send({
trusted: timetable.trusted,
source: timetable.source,
data: timetable.data,
times: times,
timetables,
times,
});
}

View File

@ -9,6 +9,7 @@ datasource db {
model Timetable {
id Int @id @unique @default(autoincrement())
title String @default("Default")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
class String

View File

@ -0,0 +1,73 @@
<script setup>
import {
CircleIcon,
CheckCircleIcon,
Edit2Icon,
TrashIcon,
CopyIcon,
} from "lucide-vue-next";
defineProps(["timetable", "editable", "selected"]);
defineEmits(["click", "edit", "delete", "copy"]);
</script>
<template>
<div class="card">
<div class="button" @click="$emit('click')">
<CheckCircleIcon v-if="selected" />
<CircleIcon v-else />
<div class="info">
<span class="name">{{ timetable.title }}</span>
<span class="detail"
>{{ $t("settings.source") }}: {{ timetable.source }}</span
>
</div>
</div>
<div class="buttons">
<Edit2Icon v-if="editable" @click="$emit('edit')" />
<TrashIcon v-if="editable" @click="$emit('delete')" />
<CopyIcon v-if="!editable" @click="$emit('copy')" />
</div>
</div>
</template>
<style scoped>
.card {
display: grid;
grid-template-columns: 1fr auto;
gap: 15px;
background-color: var(--element-color);
border-radius: 7px;
padding: 10px;
align-items: center;
cursor: pointer;
}
.button {
display: grid;
grid-template-columns: auto 1fr;
gap: 15px;
align-items: center;
}
.info {
display: grid;
grid-template-rows: 1fr 1fr;
}
.name {
font-weight: bold;
}
.detail {
opacity: 0.6;
}
.buttons {
display: flex;
gap: 7px;
opacity: 0.7;
}
</style>

View File

@ -1,5 +1,5 @@
<script setup>
import { timetable, parsedTimetable, substitutions } from "@/store";
import { timetable, times, parsedTimetable, substitutions } from "@/store";
import { getSubstitutionColor } from "@/util";
import { computed } from "vue";
@ -52,18 +52,17 @@ function isCancelled(substitution) {
}
function getTime(index) {
const times = {
...(timetable.value.times.find((e) => e.lesson == index + 1) || {}),
const lessonTimes = {
...(times.value.find((e) => e.lesson == index + 1) || {}),
};
console.log(times);
Object.keys(times).forEach((e) => {
Object.keys(lessonTimes).forEach((e) => {
if (e == "lesson") return;
const date = new Date(times[e]);
const date = new Date(lessonTimes[e]);
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
times[e] = `${hours}:${minutes}`;
lessonTimes[e] = `${hours}:${minutes}`;
});
return times;
return lessonTimes;
}
</script>
<template>

View File

@ -8,6 +8,7 @@ import SettingsView from "@/views/SettingsView.vue";
import LoginView from "@/views/LoginView.vue";
import TokenView from "@/views/TokenView.vue";
import FilteringSettings from "@/views/settings/FilteringSettings.vue";
import TimetableSettings from "@/views/settings/TimetableSettings.vue";
import TimetableGroupSettings from "@/views/settings/TimetableGroupSettings.vue";
import AppearanceSettings from "@/views/settings/AppearanceSettings.vue";
import AboutPage from "@/views/settings/AboutPage.vue";
@ -53,6 +54,11 @@ const router = createRouter({
name: "title.settings.filtering",
component: FilteringSettings,
},
{
path: "timetable",
name: "title.settings.timetable",
component: TimetableSettings,
},
{
path: "groups",
name: "title.settings.groups",

View File

@ -10,6 +10,7 @@ export const loadingFailed = ref(false);
/* Preferences */
export const classFilter = ref(localStorage.getItem("classFilter") || "none");
export const timetableId = ref(localStorage.getItem("timetableId") || "none");
export const timetableGroups = ref(
JSON.parse(localStorage.getItem("timetableGroups") || "[]")
);
@ -20,6 +21,9 @@ watch(classFilter, (newValue) => {
localStorage.setItem("classFilter", newValue);
fetchData(getNextAndPrevDay(selectedDate.value), false);
});
watch(timetableId, (newValue) => {
localStorage.setItem("timetableId", newValue);
});
watch(
timetableGroups,
(newValue) => {
@ -47,7 +51,14 @@ export const changeDay = ref(0);
export const changeDate = ref(new Date());
/* Data store */
export const timetable = ref({ trusted: true });
export const timetable = computed(() => {
const selectedTimetable = timetables.value.find(
(e) => e.id == timetableId.value
);
return selectedTimetable || { trusted: true, source: "", data: [] };
});
export const timetables = ref([]);
export const times = ref([]);
export const substitutions = ref({});
export const history = ref({});
export const classList = ref([]);
@ -92,7 +103,7 @@ export async function fetchData(days, partial) {
if (!partial) {
await fetchClassList();
loadingProgress.value = step++ / steps;
await fetchTimetable();
await fetchTimetables();
loadingProgress.value = step++ / steps;
}
for (const day of days) {
@ -117,15 +128,18 @@ export async function fetchClassList() {
classList.value = classListData;
}
export async function fetchTimetable() {
export async function fetchTimetables() {
const timetableResponse = await fetch(
`${baseUrl}/timetable?class=${classFilter.value}`
);
const timetableData = await timetableResponse.json();
if (timetableData.error) {
console.warn("API Error: " + timetableData.error);
timetable.value = { trusted: true, source: "", data: [] };
} else timetable.value = timetableData;
timetables.value = [];
} else {
timetables.value = timetableData.timetables;
times.value = timetableData.times;
}
}
export async function fetchSubstitutions(day) {

View File

@ -9,6 +9,7 @@ export const strings = {
settings: {
main: "Settings",
filtering: "Filtering",
timetable: "Manage Timetables",
groups: "Timetable Groups",
appearance: "Appearance",
about: "About",
@ -18,6 +19,7 @@ export const strings = {
heading: {
filtering: "Filtering",
timetableGroups: "Timetable Groups",
remoteTimetables: "Remote Timetables",
language: "Language",
about: "About",
theme: "Theme",
@ -37,6 +39,7 @@ export const strings = {
back: "Back",
none: "None",
version: "Version",
source: "Source",
theme: {
auto: "Auto",
dark: "Dark",
@ -88,6 +91,7 @@ export const strings = {
titles: {
loading: "Loading data",
loadingFailed: "Loading failed",
noTimetable: "No timetable",
noEntries: "No Substitutions",
noHistory: "No History",
},
@ -96,6 +100,8 @@ export const strings = {
loadingTimetable: "The timetable is still being loaded",
loadingFailed:
"The data could not be loaded. Plase check your internet connection!",
noTimetable:
"No timetable active. You can manage your timetables in the settings!",
noEntries: "There are no substitutions for this day yet",
noHistory: "No substitutions for this day have changed yet",
},
@ -113,6 +119,7 @@ export const strings = {
settings: {
main: "Einstellungen",
filtering: "Filter",
timetable: "Stundenpläne Verwalten",
groups: "Stundenplan-Gruppen",
appearance: "Aussehen",
about: "Über",
@ -121,6 +128,7 @@ export const strings = {
settings: {
heading: {
filtering: "Filter",
remoteTimetables: "Online Stundenpläne",
timetableGroups: "Stundenplan-Gruppen",
language: "Sprache",
about: "Über diese Anwendung",
@ -141,6 +149,7 @@ export const strings = {
back: "Zurück",
none: "Keine",
version: "Version",
source: "Quelle",
theme: {
auto: "Automatisch",
dark: "Dunkel",
@ -193,6 +202,7 @@ export const strings = {
titles: {
loading: "Läd noch",
loadingFailed: "Fehler",
noTimetable: "Kein Stundenplan",
noEntries: "Keine Vertretungen",
noHistory: "Noch keine Änderungen",
},
@ -201,6 +211,8 @@ export const strings = {
loadingTimetable: "Der Stundenplan wird noch geladen",
loadingFailed:
"Die Daten konnten nicht geladen werden. Bitte überprüfe deine Internetverbindung!",
noTimetable:
"Kein Stundenplan ausgewählt. Du kannst deine Stundenpläne in den Einstellungen verwalten!",
noEntries: "Es gibt noch keine Vertretungen für diesen Tag",
noHistory:
"An den Vertretungen für diesen Tag wurde noch nichts geändert",

View File

@ -3,6 +3,7 @@ import ScrollableContainer from "@/components/scrollable-container.vue";
import PageCard from "@/components/settings/page-card.vue";
import {
FilterIcon,
CalendarIcon,
CopyCheckIcon,
PaletteIcon,
InfoIcon,
@ -19,6 +20,11 @@ import {
:icon="FilterIcon"
route="settings/filtering"
/>
<PageCard
:name="$t('title.settings.timetable')"
:icon="CalendarIcon"
route="settings/timetable"
/>
<PageCard
:name="$t('title.settings.groups')"
:icon="CopyCheckIcon"

View File

@ -1,11 +1,16 @@
<script setup>
import TimetableSetup from "@/components/timetable-setup.vue";
import { classList, classFilter, timetable, loadingFailed } from "@/store";
import {
classList,
classFilter,
timetable,
loadingFailed,
loading,
} from "@/store";
import DayCarousel from "@/components/day-carousel.vue";
import TimetableList from "@/components/timetable-list.vue";
import InfoCard from "@/components/info-card.vue";
import { ClockIcon } from "lucide-vue-next";
import { CloudOffIcon } from "lucide-vue-next";
import { ClockIcon, CloudOffIcon, CalendarOffIcon } from "lucide-vue-next";
</script>
<template>
@ -20,11 +25,18 @@ import { CloudOffIcon } from "lucide-vue-next";
>
<InfoCard
class="card"
v-if="!loadingFailed"
v-if="!loadingFailed && loading"
:icon="ClockIcon"
:title="$t('infoCard.titles.loading')"
:text="$t('infoCard.texts.loadingTimetable')"
/>
<InfoCard
class="card"
v-else-if="!loadingFailed && !loading"
:icon="CalendarOffIcon"
:title="$t('infoCard.titles.noTimetable')"
:text="$t('infoCard.texts.noTimetable')"
/>
<InfoCard
class="card"
v-else

View File

@ -0,0 +1,33 @@
<script setup>
import TimetableCard from "@/components/settings/timetable-card.vue";
import { timetables, timetableId } from "@/store";
</script>
<template>
<h2>{{ $t("settings.heading.remoteTimetables") }}</h2>
<div class="list">
<TimetableCard
v-for="timetable in timetables"
:key="timetable.id"
:timetable="timetable"
:selected="timetableId == timetable.id"
:editable="true"
@click="timetableId = timetable.id"
/>
</div>
</template>
<style scoped>
h2 {
margin: 5px 0px;
}
p {
margin: 5px 0px;
}
.list {
display: grid;
gap: 8px;
}
</style>