✨ 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:
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
73
src/components/settings/timetable-card.vue
Normal file
73
src/components/settings/timetable-card.vue
Normal 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>
|
@ -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>
|
||||
|
@ -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",
|
||||
|
24
src/store.js
24
src/store.js
@ -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) {
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
33
src/views/settings/TimetableSettings.vue
Normal file
33
src/views/settings/TimetableSettings.vue
Normal 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>
|
Reference in New Issue
Block a user