🚧 Add admin api and ui for timetable management

This commit is contained in:
2023-06-20 23:10:52 +02:00
parent 0e2cc70a6b
commit 0ae553cca8
10 changed files with 307 additions and 5 deletions

View File

@ -0,0 +1,28 @@
<script setup>
import { ChevronDownIcon, ChevronRightIcon } from "lucide-vue-next";
import { ref } from "vue";
const collapsed = ref(true);
defineProps(["title"]);
</script>
<template>
<div class="title" @click="collapsed = !collapsed">
<ChevronRightIcon v-if="collapsed" />
<ChevronDownIcon v-else />
<span>{{ title }}</span>
</div>
<slot v-if="!collapsed" />
</template>
<style scoped>
.title {
display: flex;
align-items: center;
gap: 5px;
user-select: none;
font-size: 19px;
margin: 5px 0px;
}
</style>

View File

@ -11,7 +11,7 @@ import {
} from "lucide-vue-next";
import { DownloadIcon } from "lucide-vue-next";
defineProps(["timetable", "editable", "remote", "selected"]);
defineProps(["timetable", "editable", "remote", "selected", "admin"]);
defineEmits(["click", "edit", "delete", "copy", "export", "upload"]);
const deleteConfirm = ref(false);
@ -32,16 +32,17 @@ watch(deleteConfirm, (value) => {
<div class="info">
<span class="name">{{ timetable.title }}</span>
<span class="detail"
>{{ $t("settings.source") }}: {{ timetable.source }}</span
>{{ $t("settings.source") }}: {{ timetable.source }}
<span v-if="admin">, Class: {{ timetable.class }}</span></span
>
</div>
</div>
<div class="buttons">
<DownloadIcon @click="$emit('export')" />
<DownloadIcon v-if="!admin" @click="$emit('export')" />
<Edit2Icon v-if="editable && !remote" @click="$emit('edit')" />
<UploadCloudIcon v-if="editable && remote" @click="$emit('upload')" />
<CopyIcon @click="$emit('copy')" />
<CopyIcon v-if="!admin" @click="$emit('copy')" />
<TrashIcon
v-if="editable && !remote && !deleteConfirm"
@click="deleteConfirm = true"

View File

@ -2,6 +2,7 @@ import { sessionInfo } from "@/store";
export function hasPermission(permission, forValue) {
let hasPermission = false;
if (!sessionInfo.value.permissions) return false;
for (const perm of sessionInfo.value.permissions) {
if (perm == permission) hasPermission = true;
else if (perm == permission + ":" + forValue) hasPermission = true;

View File

@ -1,5 +1,6 @@
import { createRouter, createWebHistory } from "vue-router";
import { shouldLogin } from "@/store";
import { hasPermission } from "@/permission";
import { ref, watch } from "vue";
import TimetableView from "@/views/TimetableView.vue";
import SubstitutionView from "@/views/SubstitutionView.vue";
@ -13,6 +14,7 @@ import TimetableEditor from "@/views/settings/TimetableEditor.vue";
import TimetableGroupSettings from "@/views/settings/TimetableGroupSettings.vue";
import AppearanceSettings from "@/views/settings/AppearanceSettings.vue";
import KeySettings from "@/views/settings/KeySettings.vue";
import AdminSettings from "@/views/settings/AdminSettings.vue";
import AboutPage from "@/views/settings/AboutPage.vue";
const router = createRouter({
@ -76,6 +78,11 @@ const router = createRouter({
name: "title.settings.keys",
component: KeySettings,
},
{
path: "admin",
name: "title.settings.admin",
component: AdminSettings,
},
{
path: "about",
name: "title.settings.about",
@ -108,10 +115,14 @@ const router = createRouter({
});
export let lastDataRoute = ref();
router.beforeEach((_to, from) => {
router.beforeEach((to, from) => {
if (from.meta.dataView) {
lastDataRoute.value = from;
}
if (to.name == "title.settings.admin" && !hasPermission("admin")) {
alert("Nope");
router.replace("/");
}
return true;
});

View File

@ -14,6 +14,7 @@ export const strings = {
groups: "Timetable Groups",
appearance: "Appearance",
keys: "Manage Keys",
admin: "Admin Settings",
about: "About",
},
},

View File

@ -1,12 +1,14 @@
<script setup>
import ScrollableContainer from "@/components/scrollable-container.vue";
import PageCard from "@/components/settings/page-card.vue";
import { hasPermission } from "@/permission";
import {
FilterIcon,
CalendarIcon,
CopyCheckIcon,
PaletteIcon,
KeyRoundIcon,
WrenchIcon,
InfoIcon,
ChevronLeft,
} from "lucide-vue-next";
@ -41,6 +43,19 @@ import {
:icon="KeyRoundIcon"
route="settings/keys"
/>
<PageCard
v-if="hasPermission('admin')"
:name="$t('title.settings.admin')"
:icon="WrenchIcon"
style="
background-color: color-mix(
in srgb,
var(--element-color) 100%,
rgb(255, 90, 90) 40%
);
"
route="settings/admin"
/>
<PageCard
:name="$t('title.settings.about')"
:icon="InfoIcon"

View File

@ -0,0 +1,148 @@
<script setup>
import ExpandSection from "@/components/settings/expand-section.vue";
import TimetableCard from "@/components/settings/timetable-card.vue";
import { baseUrl } from "@/store";
import { PlusIcon, SaveIcon, XIcon } from "lucide-vue-next";
import { ref } from "vue";
const timetables = ref([]);
async function fetchTimetables() {
const response = await fetch(baseUrl + "/admin/timetable");
if (response.status != 200) return;
timetables.value = await response.json();
}
async function deleteTimetable(id) {
const response = await fetch(
baseUrl + "/admin/timetable?id=" + encodeURIComponent(id),
{ method: "delete" }
);
if (response.status != 200) alert("Delete failed!");
fetchTimetables();
}
const editId = ref(-1);
const timetableName = ref();
const timetableClass = ref();
const timetableSource = ref();
const timetableTrusted = ref(true);
async function createTimetable() {
const response = await fetch(baseUrl + "/admin/timetable", {
method: "post",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: timetableName.value,
class: timetableClass.value,
source: timetableSource.value,
trusted: timetableTrusted.value,
data: [],
}),
});
if (response.status != 201) alert("Post failed!");
fetchTimetables();
}
async function updateTimetable() {
const response = await fetch(
baseUrl + "/admin/timetable?id=" + encodeURIComponent(editId.value),
{
method: "put",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: timetableName.value,
class: timetableClass.value,
source: timetableSource.value,
trusted: timetableTrusted.value,
}),
}
);
if (response.status != 201) alert("Post failed!");
fetchTimetables();
}
fetchTimetables();
</script>
<template>
<h1>Admin Settings</h1>
<ExpandSection title="Timetables">
<div class="list">
<div class="create_options">
<input type="text" placeholder="Name" v-model="timetableName" />
<input type="text" placeholder="Class" v-model="timetableClass" />
<input type="text" placeholder="Source" v-model="timetableSource" />
<div>
<input type="checkbox" v-model="timetableTrusted" />
<span>Trusted</span>
</div>
<div class="button" v-if="editId == -1" @click="createTimetable()">
<PlusIcon />
<span>Create Timetable</span>
</div>
<div class="button" v-if="editId != -1" @click="updateTimetable()">
<SaveIcon />
<span>Save Timetable</span>
</div>
<div class="button" v-if="editId != -1" @click="editId = -1">
<XIcon />
<span>Cancel edit</span>
</div>
</div>
<TimetableCard
v-for="timetable in timetables"
:key="timetable"
:timetable="timetable"
:editable="true"
:selected="timetable.id == editId"
:admin="true"
@delete="deleteTimetable(timetable.id)"
@edit="
() => {
editId = timetable.id;
timetableName = timetable.title;
timetableClass = timetable.class;
timetableSource = timetable.source;
timetableTrusted = timetable.trusted;
}
"
/>
</div>
</ExpandSection>
</template>
<style scoped>
h1 {
margin: 0px 0px 10px;
}
.list {
display: flex;
flex-direction: column;
gap: 5px;
}
.button {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
background-color: var(--element-color);
padding: 5px 10px;
border-radius: 5px;
}
.create_options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
justify-content: center;
align-items: center;
gap: 10px;
padding: 10px;
cursor: pointer;
}
</style>