Merge branch 'main' into 15-calendar-preview

# Conflicts:
#	frontend/src/App.vue
#	frontend/src/view/AdditionalModules.vue
This commit is contained in:
masterelmar
2023-11-20 11:37:25 +01:00
59 changed files with 1539 additions and 811 deletions

View File

@@ -1,280 +0,0 @@
<script lang="ts" setup>
import { defineAsyncComponent, ref, Ref } from "vue";
import { Module } from "../model/module.ts";
import { fetchAllModules } from "../api/fetchCourse.ts";
import moduleStore from "../store/moduleStore.ts";
import { FilterMatchMode } from "primevue/api";
import { useDialog } from "primevue/usedialog";
const dialog = useDialog();
import router from "../router";
import { fetchModule } from "../api/fetchModule.ts";
const fetchedModules = async () => {
return await fetchAllModules();
};
const store = moduleStore();
const modules: Ref<Module[]> = ref([]);
const filters = ref({
course: {
value: null,
matchMode: FilterMatchMode.CONTAINS,
},
name: {
value: null,
matchMode: FilterMatchMode.CONTAINS,
},
prof: {
value: null,
matchMode: FilterMatchMode.CONTAINS,
}
});
//const selectedModules: Ref<Module[]> = ref([] as Module[]);
//const additionalModules: Ref<Map<string, Module>> = ref(new Map());
fetchedModules().then(
(data) =>
(modules.value = data.map((module: Module) => {
return module;
})),
);
async function nextStep() {
//selectedModules.value.forEach((module: Module) => {
// moduleStore().addModule(module);
//});
await router.push("/rename-modules");
}
const ModuleInformation = defineAsyncComponent(
() => import("./ModuleInformation.vue"),
);
async function showInfo(module: Module) {
await fetchModule(module).then((data) => {
module = data;
});
dialog.open(ModuleInformation, {
props: {
style: {
width: "50vw",
},
breakpoints: {
"960px": "75vw",
"640px": "90vw",
},
modal: true,
},
data: {
module: module,
},
});
}
/*
const display = (module: Module) => module.name + " (" + module.course + ")";
const selectAll = ref(false);
const onSelectAllChange = (event: MultiSelectAllChangeEvent) => {
if (event.checked) {
additionalModules.value = new Map(
modules.value
.filter((module: Module) => !store.hasModule(module))
.map((module: Module) => [module.uuid, module]),
);
store.overwriteModules(modules.value);
} else {
store.overwriteModules(
store.getAllModules().filter(
(module: Module) => !additionalModules.value.has(module.uuid)
)
);
additionalModules.value.clear();
}
selectAll.value = event.checked;
};
function selectChange(event : MultiSelectChangeEvent) {
let wasSelected: boolean = additionalModules.value.has(event.value.uuid);
if (event.originalEvent.target.) {
additionalModules.value.set(event.value.uuid, event.value);
} else {
additionalModules.value.delete(event.value.uuid);
}
selectAll.value = store.countModules() === modules.value.length;
}
*/
</script>
<template>
<div class="flex flex-column">
<div class="flex align-items-center justify-content-center h-4rem m-2">
<h3>
Select additional Modules that are not listed in the regular semester
for your Course
</h3>
</div>
<div class="card flex align-items-center justify-content-center m-2">
<DynamicDialog />
<DataTable
v-model:filters="filters"
:value="modules"
data-key="uuid"
paginator
:rows="10"
:rows-per-page-options="[5, 10, 20, 50]"
paginator-template="FirstPageLink PrevPageLink CurrentPageReport NextPageLink LastPageLink RowsPerPageDropdown"
current-page-report-template="{first} to {last} of {totalRecords}"
filter-display="row"
:loading="!modules.length"
loading-icon="pi pi-spinner"
:show-gridlines="true"
:striped-rows="true"
class="w-10"
>
<Column
selection-mode="multiple"
>
</Column>
<Column
field="course"
header="Course"
:show-clear-button="false"
:show-filter-menu="false"
>
<template #filter="{ filterModel, filterCallback }">
<InputText
v-model="filterModel.value"
type="text"
class="p-column-filter max-w-10rem"
@input="filterCallback()"
/>
</template>
</Column>
<Column
field="name"
header="Name"
:show-clear-button="false"
:show-filter-menu="false"
>
<template #filter="{ filterModel, filterCallback }">
<InputText
v-model="filterModel.value"
type="text"
class="p-column-filter"
@input="filterCallback()"
/>
</template>
</Column>
<Column
field="prof"
header="Professor"
:show-clear-button="false"
:show-filter-menu="false"
>
<template #filter="{ filterModel, filterCallback }">
<InputText
v-model="filterModel.value"
type="text"
class="p-column-filter"
@input="filterCallback()"
/>
</template>
</Column>
<Column header="Info">
<template #body="slotProps">
<Button
icon="pi pi-info"
severity="secondary"
rounded
outlined
aria-label="Information"
class="small-button"
@click.stop="showInfo(slotProps.data)"
></Button>
</template>
</Column>
<template #footer>
<div class="py-2 px-3">
<b>{{ store ? store.countModules() : 0 }}</b>
item{{
(store ? store.countModules() : 0) !== 1 ? "s" : ""
}}
selected.
</div>
</template>
</DataTable>
<!--
<MultiSelect
:model-value="store.getAllModules()"
@update:model-value="store.overwriteModules($event.value)"
:max-selected-labels="1"
:option-label="display"
:options="modules"
:select-all="selectAll"
:virtual-scroller-options="{ itemSize: 70 }"
class="custom-multiselect"
filter
placeholder="Select additional modules"
@change="selectChange($event)"
@selectall-change="onSelectAllChange($event)"
>
<template #option="slotProps">
<div class="flex justify-content-between w-full">
<div class="flex align-items-center justify-content-center">
<p class="text-1xl white-space-normal p-mb-0">
{{ display(slotProps.option) }}
</p>
</div>
<div class="flex align-items-center justify-content-center ml-2">
<Button
icon="pi pi-info"
severity="secondary"
rounded
outlined
aria-label="Information"
class="small-button"
@click.stop="showInfo(slotProps.option)"
></Button>
<DynamicDialog />
</div>
</div>
</template>
<template #footer>
<div class="py-2 px-3">
<b>{{ selectedModules ? selectedModules.length : 0 }}</b>
item{{
(selectedModules ? selectedModules.length : 0) > 1 ? "s" : ""
}}
selected.
</div>
</template>
</MultiSelect>
-->
</div>
<div class="flex align-items-center justify-content-center h-4rem m-2">
<Button @click="nextStep()">Next Step</Button>
</div>
</div>
</template>
<style scoped>
:deep(.custom-multiselect) {
width: 50rem;
}
:deep(.custom-multiselect li) {
height: unset;
}
</style>

View File

@@ -3,15 +3,21 @@ import tokenStore from "../store/tokenStore.ts";
import { useToast } from "primevue/usetoast";
import { onMounted } from "vue";
import router from "../router";
import { useI18n } from "vue-i18n";
const { t } = useI18n({ useScope: "global" });
const toast = useToast();
const domain = window.location.hostname;
const getLink = () =>
"https://" + domain + "/api/feed?token=" + tokenStore().token;
const show = () => {
toast.add({
severity: "info",
summary: "Info",
detail: "Link copied to clipboard",
summary: t("calendarLink.copyToastSummary"),
detail: t("calendarLink.copyToastNotification"),
life: 3000,
});
};
@@ -27,11 +33,49 @@ function rerouteIfTokenIsEmpty() {
}
function copyToClipboard() {
const text = "https://" + domain + "/api/feed?token=" + tokenStore().token;
// Copy the text inside the text field
navigator.clipboard.writeText(text);
show();
navigator.clipboard.writeText(getLink()).then(show, (err) => {
console.error("Could not copy text: ", err);
toast.add({
severity: "error",
summary: t("calendarLink.copyToastError"),
detail: t("calendarLink.copyToastErrorDetail"),
life: 3000,
});
});
}
const forwardToGoogle = () => {
window.open(
"https://calendar.google.com/calendar/u/0/r?cid=" +
encodeURI(getLink().replace("https://", "http://")),
);
};
const forwardToMicrosoft = () => {
window.open(
"https://outlook.live.com/owa?path=/calendar/action/compose&rru=addsubscription&name=HTWK%20Kalender&url=" +
encodeURI(getLink()),
);
};
const actions = [
{
label: t("calendarLink.copyToClipboard"),
icon: "pi pi-copy",
command: copyToClipboard,
},
{
label: t("calendarLink.toGoogleCalendar"),
icon: "pi pi-google",
command: forwardToGoogle,
},
{
label: t("calendarLink.toMicrosoftCalendar"),
icon: "pi pi-microsoft",
command: forwardToMicrosoft,
},
];
</script>
<template>
@@ -39,11 +83,11 @@ function copyToClipboard() {
<div class="flex flex-column">
<div class="flex align-items-center justify-content-center h-4rem m-2">
<h2>
{{ "https://" + domain + "/api/feed?token=" + tokenStore().token }}
{{ getLink() }}
</h2>
</div>
<div class="flex align-items-center justify-content-center h-4rem m-2">
<Button @click="copyToClipboard">Copy iCal Link</Button>
<div class="flex align-items-center justify-content-center m-2">
<Menu :model="actions" />
</div>
</div>
</template>

View File

@@ -1,11 +1,13 @@
<script lang="ts" setup>
import { Ref, ref } from "vue";
import { computed, ComputedRef, Ref, ref, watch } from "vue";
import {
fetchCourse,
fetchModulesByCourseAndSemester,
} from "../api/fetchCourse";
import ModuleSelection from "./ModuleSelection.vue";
import { Module } from "../model/module.ts";
import { useI18n } from "vue-i18n";
const { t } = useI18n({ useScope: "global" });
const courses = async () => {
return await fetchCourse();
@@ -13,15 +15,25 @@ const courses = async () => {
const selectedCourse: Ref<{ name: string }> = ref({ name: "" });
const countries: Ref<{ name: string }[]> = ref([]);
const semesters: Ref<{ name: string; value: string }[]> = ref([
{ name: "Wintersemester", value: "ws" },
{ name: "Sommersemester", value: "ss" },
]);
const semesters: ComputedRef<{ name: string; value: string }[]> = computed(
() => [
{ name: t("courseSelection.winterSemester"), value: "ws" },
{ name: t("courseSelection.summerSemester"), value: "ss" },
],
);
const selectedSemester: Ref<{ name: string; value: string }> = ref(
semesters.value[0],
);
//if semesters get changed, refresh the selected semester ref with a watcher
watch(
semesters,
(newValue: { name: string; value: string }[]) =>
(selectedSemester.value =
newValue[selectedSemester.value.value === "ws" ? 0 : 1]),
);
courses().then(
(data) =>
(countries.value = data.map((course) => {
@@ -43,7 +55,7 @@ async function getModules() {
<div class="flex flex-column">
<div class="flex align-items-center justify-content-center h-4rem m-2">
<h3 class="text-4xl">
Welcome to HTWKalender
{{ $t("courseSelection.headline") }}
<i
class="pi pi-calendar vertical-align-baseline"
style="font-size: 2rem"
@@ -53,7 +65,7 @@ async function getModules() {
<div
class="flex align-items-center justify-content-center h-4rem border-round m-2"
>
<h5 class="text-2xl">Please select a course</h5>
<h5 class="text-2xl">{{ $t("courseSelection.subTitle") }}</h5>
</div>
<div
class="flex align-items-center justify-content-center border-round m-2"
@@ -64,7 +76,9 @@ async function getModules() {
class="w-full md:w-25rem mx-2"
filter
option-label="name"
placeholder="Select a Course"
:placeholder="$t('courseSelection.courseDropDown')"
:empty-message="$t('courseSelection.noCoursesAvailable')"
:auto-filter-focus="true"
@change="getModules()"
></Dropdown>
<Dropdown
@@ -72,7 +86,7 @@ async function getModules() {
:options="semesters"
class="w-full md:w-25rem mx-2"
option-label="name"
placeholder="Select a Semester"
:placeholder="$t('courseSelection.semesterDropDown')"
@change="getModules()"
></Dropdown>
</div>

View File

@@ -1,277 +1,141 @@
<script lang="ts" setup></script>
<script lang="ts" setup>
</script>
<template>
<div class="flex align-items-center justify-content-center flex-column">
<div class="flex align-items-center justify-content-center h-4rem m-2">
<h1>FAQ</h1>
<h1>{{$t('faqView.headline')}}</h1>
</div>
<div class="flex flex-column col-7">
<div class="grid my-2">
<div class="col">
Wie funktioniert das Kalender erstellen mit dem HTWKalender?
{{$t('faqView.firstQuestion')}}
</div>
<div class="col">
Die Webseite ermöglicht es deinen HTWK-Stundenplan in eines deiner
bevorzugten Kalender-Verwaltungs-Programme (Outlook, Google Kalender,
etc.) einzubinden.
{{$t('faqView.firstAnswer')}}
</div>
</div>
<div class="grid my-2">
<div class="col">{{$t('faqView.secondQuestion')}}</div>
<div class="col">
{{$t('faqView.secondAnswer')}}
</div>
</div>
<div class="grid my-2">
<div class="col">Wie genau funktioniert das alles?</div>
<div class="col">
Du wählst deinen Studiengang und das gewünschte Semester aus, danach
kannst du aus den dazugehörigen Modulen wählen. Dabei kannst du
einzelne Module an/abwählen wie "Feiertage" oder Wahlpflichtmodule,
die du nicht belegst. <br />
Im letzten Schritt wird dir der Link mit dem entsprechenden Token für
den von dir erstellenten Kalenders angezeigt. Mit diesem Link kannst
du den Kalender abonnieren oder herunterladen.
</div>
</div>
<div class="grid my-2">
<div class="col">Wie kann ich den Kalender abonnieren?</div>
<div class="col">{{$t('faqView.thirdQuestion')}}</div>
<div class="col">
<Accordion>
<AccordionTab header="Google Calendar">
<AccordionTab :header="$t('faqView.thirdAnswer.tabTitle')">
<ol>
<li>Erstelle deinen Kalender und kopiere den Link.</li>
<li>{{$t('faqView.thirdAnswer.google.first')}}</li>
<li>
Bei Google Kalender selbst hast du in der linken Seitenleiste
eine Sektion namens <em>"Weitere Kalender"</em>. Dort klickst
du das kleine Pfeil-Icon rechts neben dem Schritzug. Im
daraufhin erscheinenden Menü gibt es einen Punkt
<em>"Über URL hinzufügen"</em>, den du anklicken musst.
{{$t('faqView.thirdAnswer.google.second')}}
</li>
<li>
Füge den kopierten Kalenderlink ein, klicke auf
<em>"Kalender hinzufügen"</em> und du bist fertig.
{{$t('faqView.thirdAnswer.google.third')}}
</li>
</ol>
</AccordionTab>
<AccordionTab header="Microsoft Outlook">
<p>Unter Outlook 2010:</p>
<AccordionTab :header="$t('faqView.thirdAnswer.microsoft_outlook.title')">
<p>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2010.title')}}</p>
<ol>
<li>Erstelle deinen Kalender und kopiere den Link.</li>
<li>Klicke auf den Reiter <em>Start</em>.</li>
<li>
Dort befindest sich in der Sektion
<em>Kalender verwalten</em> der Button
<em>Kalender öffnen</em>, auf den du kicken musst.
</li>
<li>
Es öffnet sich ein Kontextmenü, in dem du den Punkt
<em>Aus dem Internet</em> anklickst.
</li>
<li>
Im daraufhin erscheinenden Fenster kannst du den Link einfügen
und mit <em>OK</em> bestätigen. Eventuell musst du das
Abonnement in einem weiteren Schritt noch bestätigen. Dann
bist du fertig.
</li>
<li>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2010.first')}}</li>
<li>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2010.second')}}</li>
<li>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2010.third')}}</li>
<li>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2010.fourth')}}</li>
<li>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2010.fifth')}}</li>
</ol>
<p>Unter Outlook 2007:</p>
<p>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2007.title')}}</p>
<ol>
<li>Erstelle deinen Kalender und kopiere den Link.</li>
<li>
Unter <em>Extras</em> findest du die
<em>Kontoeinstellungen</em>.
</li>
<li>
Dort auf den Reiter <em>Internetkalender</em> klicken.
</li>
<li>Auf <em>Neu</em> klicken.</li>
<li>
Im sich öffnenden Fenster musst du den Link einfügen.
Allerdings musst du <em>http://”</em> durch
<em>webcal://”</em> austauschen.
</li>
<li>
Spätestens beim nächsten Start sollte der Kalender
aktualisiert werden und dir fortan zur Verfügung stehen.
</li>
<li>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2007.first')}}</li>
<li>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2007.second')}}</li>
<li>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2007.third')}}</li>
<li>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2007.fourth')}}</li>
<li>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2007.fifth')}}</li>
<li>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2007.sixth')}}</li>
</ol>
</AccordionTab>
<AccordionTab header="Kalender (OS X)">
<AccordionTab :header="$t('faqView.thirdAnswer.apple_osx.title')">
<ol>
<li>Erstelle deinen Kalender und kopiere den Link.</li>
<li>
Unter <em>Ablage</em> findest du den Punkt
<em>Neues Kalenderabonnement</em> (oder unter Snow Leopard
<em>Abonnieren</em>).
</li>
<li>
Im daraufhin erscheinenden Fenster musst du den kopierten Link
einfügen und <em>abonnieren</em> klicken.
</li>
<li>
Anschließend kannst du dem Kalender noch einen Namen geben und
bestimmen, wie oft er aktualisiert werden soll. Falls du
iCloud auf deinem iPhone o.ä. verwendest, empfehle ich dir bei
<em>Ort</em> unbedingt <em>iCloud</em> zu wählen. So hast
du deinen Stundenplan ohne weiteres zutun auch unterwegs immer
parat.
</li>
<li>{{$t('faqView.thirdAnswer.apple_osx.first')}}</li>
<li>{{$t('faqView.thirdAnswer.apple_osx.second')}}</li>
<li>{{$t('faqView.thirdAnswer.apple_osx.third')}}</li>
<li>{{$t('faqView.thirdAnswer.apple_osx.fourth')}}</li>
</ol>
</AccordionTab>
<AccordionTab header="Thunderbird">
<AccordionTab :header="$t('faqView.thirdAnswer.thunderbird.title')">
<ol>
<li>Erstelle deinen Kalender und kopiere den Link.</li>
<li>
Im Menü <em>Termine und Aufgaben</em> den Punkt
<em>Kalender</em> wählen.
</li>
<li>
Links siehst du die Kalenderübersicht. In diesem Bereich über
die rechte Maustaste klicken und im darauf erscheinenden
Kontextmenü <em>Neuer Kalender</em> anklicken.
</li>
<li>
Du hast die Wahl zwischen <em>Auf meinem Computer</em> und
<em>Im Netzwerk</em>. Bitte letzteres wählen und
<em>Fortsetzen</em> klicken.
</li>
<li>
Im folgenden Fenster lässt du das <em>Format</em> wie es ist
(<em>iCalender</em>).
</li>
<li>
Unter <em>"Adresse"</em> den kopierten Kalenderlink einfügen.
</li>
<li>
Anschließend kannst du noch einen Namen vergeben und weitere
Einstellungen nach Belieben vornehmen.
</li>
<li>{{ $t('faqView.thirdAnswer.thunderbird.one') }}</li>
<li>{{ $t('faqView.thirdAnswer.thunderbird.two') }}</li>
<li>{{ $t('faqView.thirdAnswer.thunderbird.three') }}</li>
<li>{{ $t('faqView.thirdAnswer.thunderbird.four') }} </li>
<li>{{ $t('faqView.thirdAnswer.thunderbird.five') }}</li>
<li>{{ $t('faqView.thirdAnswer.thunderbird.six') }}</li>
<li>{{ $t('faqView.thirdAnswer.thunderbird.seven') }}</li>
</ol>
</AccordionTab>
<AccordionTab header="IPhone">
<p>
Der einfachste Weg unter iOS ist der iCloud-Sync (siehe
Anleitung für OS X Kalender). Hier ist der andere Weg:
</p>
<AccordionTab :header="$t('faqView.thirdAnswer.iphone.title')">
<p>{{$t('faqView.thirdAnswer.iphone.description')}}</p>
<ol>
<li>Erstelle deinen Kalender und kopiere den Link.</li>
<li>Gehe in die <em>Systemeinstellungen</em>.</li>
<li>Dort wählst du <em>Mail, Kontakte, Kalender</em>.</li>
<li><em>Account hinzufügen</em> auswählen.</li>
<li>Ganz unten tippst du auf <em>Andere</em>.</li>
<li>
Der letzte Punkt ist <em>Kalenderabo hinzufügen</em>. Dort
drauftippen.
</li>
<li>
Im daraufhin erscheinenden Textfeld fügst du den Kalenderlink
ein und drückst oben auf <em>weiter</em>.
</li>
<li>
Du kannst noch eine <em>Beschreibung</em> vergeben. Den Rest
solltest du lassen, wie er ist.
</li>
<li>
Nach kurzer Zeit taucht der abonnierte Kalender in der
Kalender-App auf.
</li>
<li>{{$t('faqView.thirdAnswer.iphone.one')}}</li>
<li>{{$t('faqView.thirdAnswer.iphone.two')}}</li>
<li>{{$t('faqView.thirdAnswer.iphone.three')}}</li>
<li>{{$t('faqView.thirdAnswer.iphone.four')}}</li>
<li>{{$t('faqView.thirdAnswer.iphone.five')}}</li>
<li>{{$t('faqView.thirdAnswer.iphone.six')}}</li>
<li>{{$t('faqView.thirdAnswer.iphone.seven')}}</li>
<li>{{$t('faqView.thirdAnswer.iphone.eight')}}</li>
<li>{{$t('faqView.thirdAnswer.iphone.nine')}}</li>
</ol>
</AccordionTab>
<AccordionTab header="Android">
<p>
Unter Android ist die Synchronisierung mit dem Google Kalender
die einfachste Variante. Schaue bitte in die Google Kalender
Anleitung um zu erfahren, wie du den Kalender dort abonnierst.
</p>
<p>{{$t('faqView.thirdAnswer.android.description')}}</p>
</AccordionTab>
<AccordionTab header="Windows Phone">
<p>
Am einfachsten ist unter Windows Phone die Synchronisierung über
Outlook.com:
</p>
<p>{{$t('faqView.thirdAnswer.windows_phone.description')}}</p>
<ol>
<li>Erstelle deinen Kalender und kopiere den Link.</li>
<li>Bei Outlook.com anmelden und in den Kalender wechseln.</li>
<li>Auf <em>Subscribe</em> klicken.</li>
<li><em>Subscribe to a public calendar</em> auswählen.</li>
<li>Bei <em>Calendar URL</em> den Kalenderlink einfügen.</li>
<li>Sonstige Einstellungen nach Belieben vornehmen.</li>
<li>Auf <em>Subscribe to calendar</em> klicken.</li>
<li>
Das Windows-Phone-Gerät muss mit dem gleichen
Outlook.com-Benutzerkonto angemeldet sein. Fortan sollte die
Synchronisierung des Kalenders automatisch erfolgen.
</li>
<li>{{$t('faqView.thirdAnswer.windows_phone.one')}}</li>
<li>{{$t('faqView.thirdAnswer.windows_phone.two')}}</li>
<li>{{$t('faqView.thirdAnswer.windows_phone.three')}}</li>
<li>{{$t('faqView.thirdAnswer.windows_phone.four')}}</li>
<li>{{$t('faqView.thirdAnswer.windows_phone.five')}}</li>
<li>{{$t('faqView.thirdAnswer.windows_phone.six')}}</li>
<li>{{$t('faqView.thirdAnswer.windows_phone.seven')}}</li>
<li>{{$t('faqView.thirdAnswer.windows_phone.eight')}}</li>
</ol>
</AccordionTab>
</Accordion>
</div>
</div>
<div class="grid my-2">
<div class="col">
Kalender abonnieren? Ich will den <em>downloaden</em>!
</div>
<div class="col">
Das kannst du gern tun. Nachdem dein persönlicher Stundenplan erstellt
wurde, hast du die Möglichkeit ihn herunterzuladen. Außerdem kannst du
ihn jederzeit herunterladen, wenn du den generierten Link einfach in
deinem Browser aufrufst. <br />
Bedenke hierbei, dass heruntergeladene Kalender bzw. Stundenpläne sich
nicht aktualisieren werden. Das ist nur möglich, wenn du den Kalender
abonnierst.
</div>
<div class="col">{{$t('faqView.fourthQuestion')}}</div>
<div class="col">{{$t('faqView.fourthAnswer')}}</div>
</div>
<div class="grid my-2">
<div class="col">
Ich belege zusätzlich Module aus anderen Studiengängen und möchte
diese auch in meinem Stundenplan haben.
</div>
<div class="col">
Nachdem du die Möglichkeit hattest, die für deinen Studiengang
vorgesehenen Module aus dem Modulhandbuch auszuwählen, wirst du auf
eine zweite Seite weitergeleitet. Dort hast du die Möglichkeit,
weitere Module aus anderen Studiengängen in deinen Studienplan
einzufügen.
</div>
<div class="col">{{$t('faqView.fifthQuestion')}}</div>
<div class="col">{{$t('faqView.fifthAnswer')}}</div>
</div>
<div class="grid my-2">
<div class="col my-2">Aktualisierung Probleme mit dem Kalender?</div>
<div class="col">
Das liegt vermutlich daran, dass du ihn heruntergeladen statt
abonniert hast. Automatisch aktualisieren können sich nur Kalender,
die du abonniert hast. Eine Aktualisierung des Kalenders auf dem
Server wird täglich um 4:00 Uhr durchgeführt. So wird gewährleistet
das alle Veränderungen seitens der HTWK übernommen werden.
</div>
<div class="col my-2">{{$t('faqView.sixthQuestion')}}</div>
<div class="col">{{$t('faqView.sixthAnswer')}}</div>
</div>
<div class="grid my-2">
<div class="col">
Wie lange ist mein Stundenplan bzw. der Link dorthin gültig?
</div>
<div class="col">
Studenpläne sind erstmal nur für die ausgewählten Semester gültig. Da
durch Wahlpflichtmodule oder deine Planung sich Veränderungen ergeben
können.
</div>
<div class="col">{{$t('faqView.seventhQuestion')}}</div>
<div class="col">{{$t('faqView.seventhAnswer')}}</div>
</div>
<div class="grid my-2">
<div class="col">Preis und Entwicklung?</div>
<div class="col">
Die Kosten können durch das selbständiges Hosting vollständig
ausgelagert werden. Die Entwicklung soll als aktives Git Projekt auch
durch die Community verwaltet werden.
</div>
<div class="col">{{$t('faqView.eighthQuestion')}}</div>
<div class="col">{{$t('faqView.eighthAnswer')}}</div>
</div>
<p>
Nicht gefunden, wonach du suchst?<br />
<a href="/imprint">Kontakt aufnehmen</a>
{{$t('faqView.notFound')}}<br />
<a href="/imprint">{{$t('faqView.contact')}}</a>
</p>
</div>
</div>

View File

@@ -1,85 +0,0 @@
<script lang="ts" setup></script>
<template>
<div class="flex align-items-center justify-content-center flex-column">
<div class="flex align-items-center justify-content-center h-4rem mt-2">
<h3 class="text-4xl">Imprint</h3>
</div>
<div class="flex flex-column col-7">
<p>nach dem Telemediengesetz (TMG) der Bundesrepublik Deutschland.</p>
<h2>Kontakt</h2>
<p>
Per Email: <a href="mailto:support@ekresse.de">support@ekresse.de</a>
</p>
<h2>Adresse</h2>
<p>
Angaben gemäß § 5 TMG und verantwortlich für den Inhalt nach § 55 Abs. 2
RStV:
</p>
<p>
Elmar Kresse<br />
Philipp-Rosenthal-Straße 33<br />
04103 Leipzig
</p>
<h2>Haftungsausschluss</h2>
<h3>Haftung für Inhalte</h3>
<p>
Ich bemühe mich die Inhalte der Seite aktuell zu halten. Trotz
sorgfältiger Bearbeitung bleibt eine Haftung ausgeschlossen.
</p>
<p>
Als Diensteanbieter bin ich gemäß § 7 Abs.1 TMG für eigene Inhalte auf
diesen Seiten nach den allgemeinen Gesetzen verantwortlich.
</p>
<p>
Nach §§ 8 bis 10 TMG bin ich als Diensteanbieter jedoch nicht
verpflichtet, übermittelte oder gespeicherte fremde Informationen zu
überwachen. Bei bekannt werden von Rechtsverletzungen, werde ich diese
Inhalte umgehend entfernen. Eine diesbezügliche Haftung übernehme ich
erst ab dem Zeitpunkt der Kenntnis einer möglichen Rechtsverletzung.
</p>
<h3>Haftung für Links</h3>
<p>
Unser Angebot enthält Links zu externen Webseiten Dritter, auf deren
Inhalte wir keinen Einfluss haben. Für die Inhalte der verlinkten Seiten
ist stets der jeweilige Anbieter oder Betreiber der Seiten
verantwortlich. Für die Inhalte und die Richtigkeit der Informationen
verlinkter Websites fremder Informationsanbieter wird keine Gewähr
übernommen.
</p>
<p>
Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche
Rechtsverstöße <strong>ohne Beanstandung</strong> überprüft. Bei bekannt
werden von Rechtsverletzungen werden wir derartige Links umgehend
entfernen.
</p>
<h2>Urheberrecht</h2>
<p>
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen
Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung,
Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der
Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des
jeweiligen Autors bzw. Erstellers.
</p>
<h2>Datenschutz</h2>
<p>siehe <a href="/privacy-policy">Datenschutzerklärung</a></p>
<h2>Salvatorische Klausel</h2>
<p>
Sollte eine Bestimmung des Vertrages unwirksam sein, so bleibt die
Wirksamkeit der übrigen unberührt. Die unwirksame Bestimmung ist durch
eine Bestimmung zu ersetzen, die dem gewollten Zweck in rechtlich
zulässiger Weise am nächsten kommt. Das gleiche gilt für Vertragslücken.
</p>
</div>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,49 @@
<script lang="ts" setup>
import { computed } from "vue";
import localeStore from "../store/localeStore.ts";
import { useI18n } from "vue-i18n";
const { t } = useI18n({ useScope: "global" });
const countries = computed(() => [
{ name: t("english"), code: "en", icon: "🇬🇧" },
{ name: t("german"), code: "de", icon: "🇩🇪" },
]);
function displayIcon(code: string) {
return countries.value.find((country) => country.code === code)?.icon;
}
function displayCountry(code: string) {
return countries.value.find((country) => country.code === code)?.name;
}
function updateLocale(locale: string) {
localeStore().setLocale(locale);
}
</script>
<template>
<Dropdown
v-model="$i18n.locale"
:options="$i18n.availableLocales"
option-label="name"
placeholder="Select a Language"
class="w-full md:w-14rem"
@change="updateLocale($event.data)"
>
<template #value="slotProps">
<div v-if="slotProps.value" class="flex align-items-center">
<div class="mr-2 flag">{{ displayIcon(slotProps.value) }}</div>
<div>{{ displayCountry(slotProps.value) }}</div>
</div>
<span v-else>
{{ slotProps.placeholder }}
</span>
</template>
<template #option="slotProps">
<div class="flex align-items-center">
<div class="mr-2 flag">{{ displayIcon(slotProps.option) }}</div>
<div>{{ displayCountry(slotProps.option) }}</div>
</div>
</template>
</Dropdown>
</template>

View File

@@ -1,43 +1,69 @@
<script lang="ts" setup>
import { ref } from "vue";
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import LocaleSwitcher from "./LocaleSwitcher.vue";
const { t } = useI18n({ useScope: "global" });
const items = ref([
const items = computed(() => [
{
label: "Create Calendar",
label: t("createCalendar"),
icon: "pi pi-fw pi-plus",
url: "/",
route: "/",
},
{
label: "Edit Calendar",
label: t("editCalendar"),
icon: "pi pi-fw pi-pencil",
url: "/edit",
route: "/edit",
},
{
label: "Check Room Availability",
label: t("roomFinder"),
icon: "pi pi-fw pi-calendar",
url: "/rooms",
route: `rooms`,
},
{
label: "FAQ",
label: t("faq"),
icon: "pi pi-fw pi-book",
url: "/faq",
route: `faq`,
},
{
label: "Imprint",
label: t("imprint"),
icon: "pi pi-fw pi-id-card",
url: "/imprint",
route: `imprint`,
},
{
label: "Privacy",
url: "/privacy-policy",
label: t("privacy"),
icon: "pi pi-fw pi-exclamation-triangle",
route: `privacy-policy`,
},
]);
</script>
<template>
<Menubar :model="items" class="menubar justify-content-center">
<template #start></template>
<Menubar :model="items" class="menubar justify-content-between">
<template #start>
<img
width="35"
height="40"
src="../../public/htwk.svg"
alt="Logo"
class="h-10 w-10 mr-6"
style="margin-left: 1rem"
/>
</template>
<template #item="{ item }">
<router-link v-slot="{ navigate }" :to="item.route" custom>
<Button
:label="String(item.label)"
:icon="item.icon"
severity="secondary"
text
@click="navigate"
/>
</router-link>
</template>
<template #end>
<LocaleSwitcher></LocaleSwitcher>
</template>
</Menubar>
</template>

View File

@@ -1,10 +1,30 @@
<script lang="ts" setup>
import { inject } from "vue";
import { Module } from "../model/module.ts";
import {inject} from "vue";
import {Module} from "../model/module.ts";
import {Event} from "../model/event.ts";
import moment from "moment-timezone";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dialogRef = inject("dialogRef") as any;
const module = dialogRef.value.data.module as Module;
// formats 2023-10-26 11:45:00.000Z to DD-MM-YYYY HH:MM
function formatTimestamp(timestampString: string): string {
// Den übergebenen Zeitstempel in ein Moment-Objekt umwandeln
const timestamp = moment(timestampString);
// Die Zeitzone auf "Europe/Berlin" setzen
const berlinTime = timestamp.tz('Europe/Berlin');
// Das gewünschte Format für die Ausgabe festlegen
return berlinTime.format('DD.MM.YYYY HH:mm');
}
function sortModuleEventsByStart(events: Event[]) {
return events.sort((a, b) => {
return a.start.localeCompare(b.start);
});
}
</script>
<template>
@@ -12,24 +32,32 @@ const module = dialogRef.value.data.module as Module;
<h2>{{ module.name }}</h2>
<table>
<tr>
<td>Course: {{ module.course }}</td>
<td>{{ $t("moduleInformation.course") }}: {{ module.course }}</td>
</tr>
<tr>
<td>Person: {{ module.prof }}</td>
<td>{{ $t("moduleInformation.person") }}: {{ module.prof }}</td>
</tr>
<tr>
<td>Semester: {{ module.semester }}</td>
<td>{{ $t("moduleInformation.semester") }}: {{ module.semester }}</td>
</tr>
<tr>
<td>
<div class="card">
<DataTable :value="module.events" table-style="min-width: 50rem">
<Column field="day" header="Day"></Column>
<Column field="start" header="Start"></Column>
<Column field="end" header="End"></Column>
<Column field="rooms" header="Room"></Column>
<Column field="eventType" header="Type"></Column>
<Column field="week" header="Week"></Column>
<DataTable :value="sortModuleEventsByStart(module.events)" table-style="min-width: 50rem">
<Column field="day" :header="$t('moduleInformation.day')"></Column>
<Column field="start" :header="$t('moduleInformation.start')">
<template #body="slotProps">
{{ formatTimestamp(slotProps.data.start) }}
</template>
</Column>
<Column field="end" :header="$t('moduleInformation.end')">
<template #body="slotProps">
{{formatTimestamp( slotProps.data.end) }}
</template>
</Column>
<Column field="rooms" :header="$t('moduleInformation.room')"></Column>
<Column field="eventType" :header="$t('moduleInformation.type')"></Column>
<Column field="week" :header="$t('moduleInformation.week')"></Column>
</DataTable>
</div>
</td>

View File

@@ -64,7 +64,7 @@ function nextStep() {
:disabled="selectedModules.length < 1"
class="col-4 justify-content-center"
@click="nextStep()"
>Next Step
>{{ $t("moduleSelection.nextStep") }}
</Button>
</div>
<div class="flex align-items-center justify-content-center">
@@ -72,16 +72,19 @@ function nextStep() {
<template #header>
<div class="flex justify-content-between flex-wrap">
<div class="flex align-items-center justify-content-center">
<h3>Modules - {{ selectedModules.length }}</h3>
<h3>
{{ $t("moduleSelection.modules") }} -
{{ selectedModules.length }}
</h3>
</div>
<div class="flex align-items-center justify-content-center">
<ToggleButton
v-model="allSelected"
class="w-12rem"
off-icon="pi pi-times"
off-label="Unselect All"
:off-label="$t('moduleSelection.deselectAll')"
on-icon="pi pi-check"
on-label="Select All"
:on-label="$t('moduleSelection.selectAll')"
@click="selectAllModules(!allSelected)"
/>
</div>
@@ -89,7 +92,7 @@ function nextStep() {
</template>
<template #empty>
<p class="p-4 text-2xl font-bold text-900 empty-message">
No Modules found for this course
{{ $t("moduleSelection.noModulesAvailable") }}
</p>
</template>
<template #list="slotProps">
@@ -112,9 +115,9 @@ function nextStep() {
v-model="modulesWithSelection[slotProps.index].selected"
class="w-9rem"
off-icon="pi pi-times"
off-label="Unselected"
:off-label="$t('moduleSelection.unselected')"
on-icon="pi pi-check"
on-label="Selected"
:on-label="$t('moduleSelection.selected')"
/>
</div>
</div>

View File

@@ -1,18 +1,30 @@
<script setup lang="ts">
import { Ref, ref } from "vue";
import { computed, Ref, ref } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n({ useScope: "global" });
const helpVisible: Ref<boolean> = ref(false);
const placeholders = ref([
const placeholders = computed(() => [
{
placeholder: "%t",
description: "Event Type",
examples: "V = Vorlesung, S = Seminar, P = Praktikum/Prüfung",
description: t("moduleTemplateDialog.eventTyp"),
examples:
"V = " +
t("moduleTemplateDialog.lecture") +
", S = " +
t("moduleTemplateDialog.seminar") +
", P = " +
t("moduleTemplateDialog.exam"),
},
{
placeholder: "%p",
description: "Mandatory",
examples: "w = optional, p = mandatory",
description: t("moduleTemplateDialog.mandatory"),
examples:
"w = " +
t("moduleTemplateDialog.optional") +
", p = " +
t("moduleTemplateDialog.mandatory"),
},
]);
</script>
@@ -28,20 +40,30 @@ const placeholders = ref([
aria-label="Help"
@click="helpVisible = true"
/>
<Dialog v-model:visible="helpVisible" header="Module configuration">
<Dialog
v-model:visible="helpVisible"
:header="$t('moduleTemplateDialog.moduleConfiguration')"
>
<p>
Here you can rename your modules to your liking. This will be the name of
the event in your calendar.
{{ t("moduleTemplateDialog.explanationOne") }}
</p>
<p>You can use the following placeholders in your module names:</p>
<p>{{ t("moduleTemplateDialog.tableDescription") }}</p>
<DataTable :value="placeholders">
<Column field="placeholder" header="Placeholder"></Column>
<Column field="description" header="Description"></Column>
<Column field="examples" header="Examples"></Column>
<Column
field="placeholder"
:header="$t('moduleTemplateDialog.placeholder')"
></Column>
<Column
field="description"
:header="$t('moduleTemplateDialog.description')"
></Column>
<Column
field="examples"
:header="$t('moduleTemplateDialog.examples')"
></Column>
</DataTable>
<p>
Additionally, you can toggle notifications for each module. If you do so,
you will be notified 15 minutes before the event starts.
{{ t("moduleTemplateDialog.explanationTwo") }}
</p>
</Dialog>
</template>

View File

@@ -1,646 +0,0 @@
<script lang="ts" setup></script>
<template>
<div class="flex align-items-center justify-content-center flex-column">
<div class="flex align-items-center justify-content-center h-4rem mt-2">
<h3 class="text-4xl">Privacy policy</h3>
</div>
<div class="flex flex-column col-7">
<h1>Datenschutzerklärung</h1>
<p>Stand: 19. September 2023</p>
<p>
Mit der folgenden Datenschutzerklärung möchten wir Sie darüber
aufklären, welche Arten Ihrer personenbezogenen Daten (nachfolgend auch
kurz als "Daten“ bezeichnet) wir zu welchen Zwecken und in welchem
Umfang im Rahmen der Bereitstellung unserer Applikation verarbeiten. Die
verwendeten Begriffe sind nicht geschlechtsspezifisch.
</p>
<h2>Inhaltsübersicht</h2>
<ul class="index">
<li><a class="index-link" href="#m3">Verantwortlicher</a></li>
<li>
<a class="index-link" href="#mOverview"
>Übersicht der Verarbeitungen</a
>
</li>
<li>
<a class="index-link" href="#m2427">Maßgebliche Rechtsgrundlagen</a>
</li>
<li><a class="index-link" href="#m27">Sicherheitsmaßnahmen</a></li>
<li>
<a class="index-link" href="#m25"
>Übermittlung von personenbezogenen Daten</a
>
</li>
<li>
<a class="index-link" href="#m24">Internationale Datentransfers</a>
</li>
<li>
<a class="index-link" href="#m10">Rechte der betroffenen Personen</a>
</li>
<li><a class="index-link" href="#m134">Einsatz von Cookies</a></li>
<li>
<a class="index-link" href="#m225"
>Bereitstellung des Onlineangebotes und Webhosting</a
>
</li>
<li>
<a class="index-link" href="#m182">Kontakt- und Anfragenverwaltung</a>
</li>
</ul>
<h2 id="m3">Verantwortlicher</h2>
<p>
Elmar Kresse<br />Philipp-Rosenthal-Straße 33<br />04103, Leipzig,
Deutschland
</p>
E-Mail-Adresse:
<p><a href="mailto:support@ekresse.de">support@ekresse.de</a></p>
<h2 id="mOverview">Übersicht der Verarbeitungen</h2>
<p>
Die nachfolgende Übersicht fasst die Arten der verarbeiteten Daten und
die Zwecke ihrer Verarbeitung zusammen und verweist auf die betroffenen
Personen.
</p>
<h3>Arten der verarbeiteten Daten</h3>
<ul>
<li>Kontaktdaten.</li>
<li>Inhaltsdaten.</li>
<li>Nutzungsdaten.</li>
<li>Meta-, Kommunikations- und Verfahrensdaten.</li>
</ul>
<h3>Kategorien betroffener Personen</h3>
<ul>
<li>Kommunikationspartner.</li>
<li>Nutzer.</li>
</ul>
<h3>Zwecke der Verarbeitung</h3>
<ul>
<li>Kontaktanfragen und Kommunikation.</li>
<li>Sicherheitsmaßnahmen.</li>
<li>Verwaltung und Beantwortung von Anfragen.</li>
<li>Feedback.</li>
<li>
Bereitstellung unseres Onlineangebotes und Nutzerfreundlichkeit.
</li>
<li>Informationstechnische Infrastruktur.</li>
</ul>
<h2 id="m2427">Maßgebliche Rechtsgrundlagen</h2>
<p>
<strong>Maßgebliche Rechtsgrundlagen nach der DSGVO: </strong>Im
Folgenden erhalten Sie eine Übersicht der Rechtsgrundlagen der DSGVO,
auf deren Basis wir personenbezogene Daten verarbeiten. Bitte nehmen Sie
zur Kenntnis, dass neben den Regelungen der DSGVO nationale
Datenschutzvorgaben in Ihrem bzw. unserem Wohn- oder Sitzland gelten
können. Sollten ferner im Einzelfall speziellere Rechtsgrundlagen
maßgeblich sein, teilen wir Ihnen diese in der Datenschutzerklärung mit.
</p>
<ul>
<li>
<strong>Einwilligung (Art. 6 Abs. 1 S. 1 lit. a) DSGVO)</strong> - Die
betroffene Person hat ihre Einwilligung in die Verarbeitung der sie
betreffenden personenbezogenen Daten für einen spezifischen Zweck oder
mehrere bestimmte Zwecke gegeben.
</li>
<li>
<strong
>Berechtigte Interessen (Art. 6 Abs. 1 S. 1 lit. f) DSGVO)</strong
>
- Die Verarbeitung ist zur Wahrung der berechtigten Interessen des
Verantwortlichen oder eines Dritten erforderlich, sofern nicht die
Interessen oder Grundrechte und Grundfreiheiten der betroffenen
Person, die den Schutz personenbezogener Daten erfordern, überwiegen.
</li>
</ul>
<p>
<strong>Nationale Datenschutzregelungen in Deutschland: </strong
>Zusätzlich zu den Datenschutzregelungen der DSGVO gelten nationale
Regelungen zum Datenschutz in Deutschland. Hierzu gehört insbesondere
das Gesetz zum Schutz vor Missbrauch personenbezogener Daten bei der
Datenverarbeitung (Bundesdatenschutzgesetz BDSG). Das BDSG enthält
insbesondere Spezialregelungen zum Recht auf Auskunft, zum Recht auf
Löschung, zum Widerspruchsrecht, zur Verarbeitung besonderer Kategorien
personenbezogener Daten, zur Verarbeitung für andere Zwecke und zur
Übermittlung sowie automatisierten Entscheidungsfindung im Einzelfall
einschließlich Profiling. Ferner können Landesdatenschutzgesetze der
einzelnen Bundesländer zur Anwendung gelangen.
</p>
<p>
<strong>Hinweis auf Geltung DSGVO und Schweizer DSG: </strong>Diese
Datenschutzhinweise dienen sowohl der Informationserteilung nach dem
schweizerischen Bundesgesetz über den Datenschutz (Schweizer DSG) als
auch nach der Datenschutzgrundverordnung (DSGVO). Aus diesem Grund
bitten wir Sie zu beachten, dass aufgrund der breiteren räumlichen
Anwendung und Verständlichkeit die Begriffe der DSGVO verwendet werden.
Insbesondere statt der im Schweizer DSG verwendeten Begriffe
„Bearbeitung" von Personendaten", "überwiegendes Interesse" und
"besonders schützenswerte Personendaten" werden die in der DSGVO
verwendeten Begriffe „Verarbeitung" von personenbezogenen Daten" sowie
"berechtigtes Interesse" und "besondere Kategorien von Daten" verwendet.
Die gesetzliche Bedeutung der Begriffe wird jedoch im Rahmen der Geltung
des Schweizer DSG weiterhin nach dem Schweizer DSG bestimmt.
</p>
<h2 id="m27">Sicherheitsmaßnahmen</h2>
<p>
Wir treffen nach Maßgabe der gesetzlichen Vorgaben unter
Berücksichtigung des Stands der Technik, der Implementierungskosten und
der Art, des Umfangs, der Umstände und der Zwecke der Verarbeitung sowie
der unterschiedlichen Eintrittswahrscheinlichkeiten und des Ausmaßes der
Bedrohung der Rechte und Freiheiten natürlicher Personen geeignete
technische und organisatorische Maßnahmen, um ein dem Risiko
angemessenes Schutzniveau zu gewährleisten.
</p>
<p>
Zu den Maßnahmen gehören insbesondere die Sicherung der Vertraulichkeit,
Integrität und Verfügbarkeit von Daten durch Kontrolle des physischen
und elektronischen Zugangs zu den Daten als auch des sie betreffenden
Zugriffs, der Eingabe, der Weitergabe, der Sicherung der Verfügbarkeit
und ihrer Trennung. Des Weiteren haben wir Verfahren eingerichtet, die
eine Wahrnehmung von Betroffenenrechten, die Löschung von Daten und
Reaktionen auf die Gefährdung der Daten gewährleisten. Ferner
berücksichtigen wir den Schutz personenbezogener Daten bereits bei der
Entwicklung bzw. Auswahl von Hardware, Software sowie Verfahren
entsprechend dem Prinzip des Datenschutzes, durch Technikgestaltung und
durch datenschutzfreundliche Voreinstellungen.
</p>
<p>
TLS-Verschlüsselung (https): Um Ihre via unserem Online-Angebot
übermittelten Daten zu schützen, nutzen wir eine TLS-Verschlüsselung.
Sie erkennen derart verschlüsselte Verbindungen an dem Präfix https://
in der Adresszeile Ihres Browsers.
</p>
<h2 id="m25">Übermittlung von personenbezogenen Daten</h2>
<p>
Im Rahmen unserer Verarbeitung von personenbezogenen Daten kommt es vor,
dass die Daten an andere Stellen, Unternehmen, rechtlich selbstständige
Organisationseinheiten oder Personen übermittelt oder sie ihnen
gegenüber offengelegt werden. Zu den Empfängern dieser Daten können z.B.
mit IT-Aufgaben beauftragte Dienstleister oder Anbieter von Diensten und
Inhalten, die in eine Webseite eingebunden werden, gehören. In solchen
Fällen beachten wir die gesetzlichen Vorgaben und schließen insbesondere
entsprechende Verträge bzw. Vereinbarungen, die dem Schutz Ihrer Daten
dienen, mit den Empfängern Ihrer Daten ab.
</p>
<h2 id="m24">Internationale Datentransfers</h2>
<p>
Datenverarbeitung in Drittländern: Sofern wir Daten in einem Drittland
(d.h., außerhalb der Europäischen Union (EU), des Europäischen
Wirtschaftsraums (EWR)) verarbeiten oder die Verarbeitung im Rahmen der
Inanspruchnahme von Diensten Dritter oder der Offenlegung bzw.
Übermittlung von Daten an andere Personen, Stellen oder Unternehmen
stattfindet, erfolgt dies nur im Einklang mit den gesetzlichen Vorgaben.
Sofern das Datenschutzniveau in dem Drittland mittels eines
Angemessenheitsbeschlusses anerkannt wurde (Art. 45 DSGVO), dient dieser
als Grundlage des Datentransfers. Im Übrigen erfolgen Datentransfers nur
dann, wenn das Datenschutzniveau anderweitig gesichert ist, insbesondere
durch Standardvertragsklauseln (Art. 46 Abs. 2 lit. c) DSGVO),
ausdrückliche Einwilligung oder im Fall vertraglicher oder gesetzlich
erforderlicher Übermittlung (Art. 49 Abs. 1 DSGVO). Im Übrigen teilen
wir Ihnen die Grundlagen der Drittlandübermittlung bei den einzelnen
Anbietern aus dem Drittland mit, wobei die Angemesenheitsbeschlüsse als
Grundlagen vorrangig gelten. Informationen zu Drittlandtransfers und
vorliegenden Angemessenheitsbeschlüssen können dem Informationsangebot
der EU-Kommission entnommen werden:
<a
href="https://ec.europa.eu/info/law/law-topic/data-protection/international-dimension-data-protection_de"
target="_blank"
>https://ec.europa.eu/info/law/law-topic/data-protection/international-dimension-data-protection_de.</a
>
</p>
<p>
EU-US Trans-Atlantic Data Privacy Framework: Im Rahmen des sogenannten
Data Privacy Framework" (DPF) hat die EU-Kommission das
Datenschutzniveau ebenfalls für bestimmte Unternehmen aus den USA im
Rahmen der Angemessenheitsbeschlusses vom 10.07.2023 als sicher
anerkannt. Die Liste der zertifizierten Unternehmen als auch weitere
Informationen zu dem DPF können Sie der Webseite des Handelsministeriums
der USA unter
<a href="https://www.dataprivacyframework.gov/" target="_blank"
>https://www.dataprivacyframework.gov/</a
>
(in Englisch) entnehmen. Wir informieren Sie im Rahmen der
Datenschutzhinweise welche von uns eingesetzten Diensteanbieter unter
dem Data Privacy Framework zertifiziert sind.
</p>
<h2 id="m10">Rechte der betroffenen Personen</h2>
<p>
Rechte der betroffenen Personen aus der DSGVO: Ihnen stehen als
Betroffene nach der DSGVO verschiedene Rechte zu, die sich insbesondere
aus Art. 15 bis 21 DSGVO ergeben:
</p>
<ul>
<li>
<strong
>Widerspruchsrecht: Sie haben das Recht, aus Gründen, die sich aus
Ihrer besonderen Situation ergeben, jederzeit gegen die Verarbeitung
der Sie betreffenden personenbezogenen Daten, die aufgrund von Art.
6 Abs. 1 lit. e oder f DSGVO erfolgt, Widerspruch einzulegen; dies
gilt auch für ein auf diese Bestimmungen gestütztes Profiling.
Werden die Sie betreffenden personenbezogenen Daten verarbeitet, um
Direktwerbung zu betreiben, haben Sie das Recht, jederzeit
Widerspruch gegen die Verarbeitung der Sie betreffenden
personenbezogenen Daten zum Zwecke derartiger Werbung einzulegen;
dies gilt auch für das Profiling, soweit es mit solcher
Direktwerbung in Verbindung steht.</strong
>
</li>
<li>
<strong>Widerrufsrecht bei Einwilligungen:</strong> Sie haben das
Recht, erteilte Einwilligungen jederzeit zu widerrufen.
</li>
<li>
<strong>Auskunftsrecht:</strong> Sie haben das Recht, eine Bestätigung
darüber zu verlangen, ob betreffende Daten verarbeitet werden und auf
Auskunft über diese Daten sowie auf weitere Informationen und Kopie
der Daten entsprechend den gesetzlichen Vorgaben.
</li>
<li>
<strong>Recht auf Berichtigung:</strong> Sie haben entsprechend den
gesetzlichen Vorgaben das Recht, die Vervollständigung der Sie
betreffenden Daten oder die Berichtigung der Sie betreffenden
unrichtigen Daten zu verlangen.
</li>
<li>
<strong
>Recht auf Löschung und Einschränkung der Verarbeitung:</strong
>
Sie haben nach Maßgabe der gesetzlichen Vorgaben das Recht, zu
verlangen, dass Sie betreffende Daten unverzüglich gelöscht werden,
bzw. alternativ nach Maßgabe der gesetzlichen Vorgaben eine
Einschränkung der Verarbeitung der Daten zu verlangen.
</li>
<li>
<strong>Recht auf Datenübertragbarkeit:</strong> Sie haben das Recht,
Sie betreffende Daten, die Sie uns bereitgestellt haben, nach Maßgabe
der gesetzlichen Vorgaben in einem strukturierten, gängigen und
maschinenlesbaren Format zu erhalten oder deren Übermittlung an einen
anderen Verantwortlichen zu fordern.
</li>
<li>
<strong>Beschwerde bei Aufsichtsbehörde:</strong> Sie haben
unbeschadet eines anderweitigen verwaltungsrechtlichen oder
gerichtlichen Rechtsbehelfs das Recht auf Beschwerde bei einer
Aufsichtsbehörde, insbesondere in dem Mitgliedstaat ihres gewöhnlichen
Aufenthaltsorts, ihres Arbeitsplatzes oder des Orts des mutmaßlichen
Verstoßes, wenn Sie der Ansicht sind, dass die Verarbeitung der Sie
betreffenden personenbezogenen Daten gegen die Vorgaben der DSGVO
verstößt.
</li>
</ul>
<h2 id="m134">Einsatz von Cookies</h2>
<p>
Cookies sind kleine Textdateien, bzw. sonstige Speichervermerke, die
Informationen auf Endgeräten speichern und Informationen aus den
Endgeräten auslesen. Z.B. um den Login-Status in einem Nutzerkonto,
einen Warenkorbinhalt in einem E-Shop, die aufgerufenen Inhalte oder
verwendete Funktionen eines Onlineangebotes speichern. Cookies können
ferner zu unterschiedlichen Zwecken eingesetzt werden, z.B. zu Zwecken
der Funktionsfähigkeit, Sicherheit und Komfort von Onlineangeboten sowie
der Erstellung von Analysen der Besucherströme.
</p>
<p>
<strong>Hinweise zur Einwilligung: </strong>Wir setzen Cookies im
Einklang mit den gesetzlichen Vorschriften ein. Daher holen wir von den
Nutzern eine vorhergehende Einwilligung ein, außer wenn diese gesetzlich
nicht gefordert ist. Eine Einwilligung ist insbesondere nicht notwendig,
wenn das Speichern und das Auslesen der Informationen, also auch von
Cookies, unbedingt erforderlich sind, um dem den Nutzern einen von ihnen
ausdrücklich gewünschten Telemediendienst (also unser Onlineangebot) zur
Verfügung zu stellen. Zu den unbedingt erforderlichen Cookies gehören in
der Regel Cookies mit Funktionen, die der Anzeige und Lauffähigkeit des
Onlineangebotes , dem Lastausgleich, der Sicherheit, der Speicherung der
Präferenzen und Auswahlmöglichkeiten der Nutzer oder ähnlichen mit der
Bereitstellung der Haupt- und Nebenfunktionen des von den Nutzern
angeforderten Onlineangebotes zusammenhängenden Zwecken dienen. Die
widerrufliche Einwilligung wird gegenüber den Nutzern deutlich
kommuniziert und enthält die Informationen zu der jeweiligen
Cookie-Nutzung.
</p>
<p>
<strong>Hinweise zu datenschutzrechtlichen Rechtsgrundlagen: </strong
>Auf welcher datenschutzrechtlichen Rechtsgrundlage wir die
personenbezogenen Daten der Nutzer mit Hilfe von Cookies verarbeiten,
hängt davon ab, ob wir Nutzer um eine Einwilligung bitten. Falls die
Nutzer einwilligen, ist die Rechtsgrundlage der Verarbeitung Ihrer Daten
die erklärte Einwilligung. Andernfalls werden die mithilfe von Cookies
verarbeiteten Daten auf Grundlage unserer berechtigten Interessen (z.B.
an einem betriebswirtschaftlichen Betrieb unseres Onlineangebotes und
Verbesserung seiner Nutzbarkeit) verarbeitet oder, wenn dies im Rahmen
der Erfüllung unserer vertraglichen Pflichten erfolgt, wenn der Einsatz
von Cookies erforderlich ist, um unsere vertraglichen Verpflichtungen zu
erfüllen. Zu welchen Zwecken die Cookies von uns verarbeitet werden,
darüber klären wir im Laufe dieser Datenschutzerklärung oder im Rahmen
von unseren Einwilligungs- und Verarbeitungsprozessen auf.
</p>
<p>
<strong>Speicherdauer:</strong>Im Hinblick auf die Speicherdauer werden
die folgenden Arten von Cookies unterschieden:
</p>
<ul>
<li>
<strong
>Temporäre Cookies (auch: Session- oder Sitzungs-Cookies):</strong
>
Temporäre Cookies werden spätestens gelöscht, nachdem ein Nutzer ein
Online-Angebot verlassen und sein Endgerät (z.B. Browser oder mobile
Applikation) geschlossen hat.
</li>
<li>
<strong>Permanente Cookies:</strong> Permanente Cookies bleiben auch
nach dem Schließen des Endgerätes gespeichert. So können
beispielsweise der Login-Status gespeichert oder bevorzugte Inhalte
direkt angezeigt werden, wenn der Nutzer eine Website erneut besucht.
Ebenso können die mit Hilfe von Cookies erhobenen Daten der Nutzer zur
Reichweitenmessung verwendet werden. Sofern wir Nutzern keine
expliziten Angaben zur Art und Speicherdauer von Cookies mitteilen
(z.B. im Rahmen der Einholung der Einwilligung), sollten Nutzer davon
ausgehen, dass Cookies permanent sind und die Speicherdauer bis zu
zwei Jahre betragen kann.
</li>
</ul>
<p>
<strong
>Allgemeine Hinweise zum Widerruf und Widerspruch (sog. "Opt-Out"): </strong
>Nutzer können die von ihnen abgegebenen Einwilligungen jederzeit
widerrufen und der Verarbeitung entsprechend den gesetzlichen Vorgaben
widersprechen. Hierzu können Nutzer unter anderem die Verwendung von
Cookies in den Einstellungen ihres Browsers einschränken (wobei dadurch
auch die Funktionalität unseres Onlineangebotes eingeschränkt sein
kann). Ein Widerspruch gegen die Verwendung von Cookies zu
Online-Marketing-Zwecken kann auch über die Websites
<a href="https://optout.aboutads.info/" target="_new"
>https://optout.aboutads.info</a
>
und
<a href="https://www.youronlinechoices.com/" target="_new"
>https://www.youronlinechoices.com/</a
>
erklärt werden.
</p>
<ul class="m-elements">
<li class="">
<strong>Rechtsgrundlagen:</strong> Berechtigte Interessen (Art. 6 Abs.
1 S. 1 lit. f) DSGVO). Einwilligung (Art. 6 Abs. 1 S. 1 lit. a)
DSGVO).
</li>
</ul>
<p>
<strong
>Weitere Hinweise zu Verarbeitungsprozessen, Verfahren und
Diensten:</strong
>
</p>
<ul class="m-elements">
<li>
<strong
>Verarbeitung von Cookie-Daten auf Grundlage einer Einwilligung: </strong
>Wir setzen ein Verfahren zum Cookie-Einwilligungs-Management ein, in
dessen Rahmen die Einwilligungen der Nutzer in den Einsatz von
Cookies, bzw. der im Rahmen des
Cookie-Einwilligungs-Management-Verfahrens genannten Verarbeitungen
und Anbieter eingeholt sowie von den Nutzern verwaltet und widerrufen
werden können. Hierbei wird die Einwilligungserklärung gespeichert, um
deren Abfrage nicht erneut wiederholen zu müssen und die Einwilligung
entsprechend der gesetzlichen Verpflichtung nachweisen zu können. Die
Speicherung kann serverseitig und/oder in einem Cookie (sogenanntes
Opt-In-Cookie, bzw. mithilfe vergleichbarer Technologien) erfolgen, um
die Einwilligung einem Nutzer, bzw. dessen Gerät zuordnen zu können.
Vorbehaltlich individueller Angaben zu den Anbietern von
Cookie-Management-Diensten, gelten die folgenden Hinweise: Die Dauer
der Speicherung der Einwilligung kann bis zu zwei Jahren betragen.
Hierbei wird ein pseudonymer Nutzer-Identifikator gebildet und mit dem
Zeitpunkt der Einwilligung, Angaben zur Reichweite der Einwilligung
(z.B. welche Kategorien von Cookies und/oder Diensteanbieter) sowie
dem Browser, System und verwendeten Endgerät gespeichert;
<span class=""
><strong>Rechtsgrundlagen:</strong> Einwilligung (Art. 6 Abs. 1 S. 1
lit. a) DSGVO).</span
>
</li>
</ul>
<h2 id="m225">Bereitstellung des Onlineangebotes und Webhosting</h2>
<p>
Wir verarbeiten die Daten der Nutzer, um ihnen unsere Online-Dienste zur
Verfügung stellen zu können. Zu diesem Zweck verarbeiten wir die
IP-Adresse des Nutzers, die notwendig ist, um die Inhalte und Funktionen
unserer Online-Dienste an den Browser oder das Endgerät der Nutzer zu
übermitteln.
</p>
<ul class="m-elements">
<li>
<strong>Verarbeitete Datenarten:</strong> Nutzungsdaten (z.B. besuchte
Webseiten, Interesse an Inhalten, Zugriffszeiten); Meta-,
Kommunikations- und Verfahrensdaten (z.B. IP-Adressen, Zeitangaben,
Identifikationsnummern, Einwilligungsstatus).
</li>
<li>
<strong>Betroffene Personen:</strong> Nutzer (z..B. Webseitenbesucher,
Nutzer von Onlinediensten).
</li>
<li>
<strong>Zwecke der Verarbeitung:</strong> Bereitstellung unseres
Onlineangebotes und Nutzerfreundlichkeit; Informationstechnische
Infrastruktur (Betrieb und Bereitstellung von Informationssystemen und
technischen Geräten (Computer, Server etc.).). Sicherheitsmaßnahmen.
</li>
<li class="">
<strong>Rechtsgrundlagen:</strong> Berechtigte Interessen (Art. 6 Abs.
1 S. 1 lit. f) DSGVO).
</li>
</ul>
<p>
<strong
>Weitere Hinweise zu Verarbeitungsprozessen, Verfahren und
Diensten:</strong
>
</p>
<ul class="m-elements">
<li>
<strong
>Bereitstellung Onlineangebot auf eigener/ dedizierter
Serverhardware: </strong
>Für die Bereitstellung unseres Onlineangebotes nutzen wir von uns
betriebene Serverhardware sowie den damit verbundenen Speicherplatz,
die Rechenkapazität und die Software;
<span class=""
><strong>Rechtsgrundlagen:</strong> Berechtigte Interessen (Art. 6
Abs. 1 S. 1 lit. f) DSGVO).</span
>
</li>
<li>
<strong>Erhebung von Zugriffsdaten und Logfiles: </strong>Der Zugriff
auf unser Onlineangebot wird in Form von so genannten
"Server-Logfiles" protokolliert. Zu den Serverlogfiles können die
Adresse und Name der abgerufenen Webseiten und Dateien, Datum und
Uhrzeit des Abrufs, übertragene Datenmengen, Meldung über
erfolgreichen Abruf, Browsertyp nebst Version, das Betriebssystem des
Nutzers, Referrer URL (die zuvor besuchte Seite) und im Regelfall
IP-Adressen und der anfragende Provider gehören. Die Serverlogfiles
können zum einen zu Zwecken der Sicherheit eingesetzt werden, z.B., um
eine Überlastung der Server zu vermeiden (insbesondere im Fall von
missbräuchlichen Angriffen, sogenannten DDoS-Attacken) und zum
anderen, um die Auslastung der Server und ihre Stabilität
sicherzustellen;
<span class=""
><strong>Rechtsgrundlagen:</strong> Berechtigte Interessen (Art. 6
Abs. 1 S. 1 lit. f) DSGVO). </span
><strong>Löschung von Daten:</strong> Logfile-Informationen werden für
die Dauer von maximal 30 Tagen gespeichert und danach gelöscht oder
anonymisiert. Daten, deren weitere Aufbewahrung zu Beweiszwecken
erforderlich ist, sind bis zur endgültigen Klärung des jeweiligen
Vorfalls von der Löschung ausgenommen.
</li>
</ul>
<h2 id="m182">Kontakt- und Anfragenverwaltung</h2>
<p>
Bei der Kontaktaufnahme mit uns (z.B. per Post, Kontaktformular, E-Mail,
Telefon oder via soziale Medien) sowie im Rahmen bestehender Nutzer- und
Geschäftsbeziehungen werden die Angaben der anfragenden Personen
verarbeitet soweit dies zur Beantwortung der Kontaktanfragen und
etwaiger angefragter Maßnahmen erforderlich ist.
</p>
<ul class="m-elements">
<li>
<strong>Verarbeitete Datenarten:</strong> Kontaktdaten (z.B. E-Mail,
Telefonnummern); Inhaltsdaten (z.B. Eingaben in Onlineformularen);
Nutzungsdaten (z.B. besuchte Webseiten, Interesse an Inhalten,
Zugriffszeiten); Meta-, Kommunikations- und Verfahrensdaten (z.B.
IP-Adressen, Zeitangaben, Identifikationsnummern,
Einwilligungsstatus).
</li>
<li><strong>Betroffene Personen:</strong> Kommunikationspartner.</li>
<li>
<strong>Zwecke der Verarbeitung:</strong> Kontaktanfragen und
Kommunikation; Verwaltung und Beantwortung von Anfragen; Feedback
(z.B. Sammeln von Feedback via Online-Formular). Bereitstellung
unseres Onlineangebotes und Nutzerfreundlichkeit.
</li>
<li class="">
<strong>Rechtsgrundlagen:</strong> Berechtigte Interessen (Art. 6 Abs.
1 S. 1 lit. f) DSGVO).
</li>
</ul>
<h2 id="m10">Rechte der betroffenen Personen</h2>
<p>
Ihnen stehen als Betroffene nach der DSGVO verschiedene Rechte zu, die
sich insbesondere aus Art. 15 bis 21 DSGVO ergeben:
</p>
<ul>
<li>
<strong
>Widerspruchsrecht: Sie haben das Recht, aus Gründen, die sich aus
Ihrer besonderen Situation ergeben, jederzeit gegen die Verarbeitung
der Sie betreffenden personenbezogenen Daten, die aufgrund von Art.
6 Abs. 1 lit. e oder f DSGVO erfolgt, Widerspruch einzulegen; dies
gilt auch für ein auf diese Bestimmungen gestütztes Profiling.
Werden die Sie betreffenden personenbezogenen Daten verarbeitet, um
Direktwerbung zu betreiben, haben Sie das Recht, jederzeit
Widerspruch gegen die Verarbeitung der Sie betreffenden
personenbezogenen Daten zum Zwecke derartiger Werbung einzulegen;
dies gilt auch für das Profiling, soweit es mit solcher
Direktwerbung in Verbindung steht.</strong
>
</li>
<li>
<strong>Widerrufsrecht bei Einwilligungen:</strong> Sie haben das
Recht, erteilte Einwilligungen jederzeit zu widerrufen.
</li>
<li>
<strong>Auskunftsrecht:</strong> Sie haben das Recht, eine Bestätigung
darüber zu verlangen, ob betreffende Daten verarbeitet werden und auf
Auskunft über diese Daten sowie auf weitere Informationen und Kopie
der Daten entsprechend den gesetzlichen Vorgaben.
</li>
<li>
<strong>Recht auf Berichtigung:</strong> Sie haben entsprechend den
gesetzlichen Vorgaben das Recht, die Vervollständigung der Sie
betreffenden Daten oder die Berichtigung der Sie betreffenden
unrichtigen Daten zu verlangen.
</li>
<li>
<strong
>Recht auf Löschung und Einschränkung der Verarbeitung:</strong
>
Sie haben nach Maßgabe der gesetzlichen Vorgaben das Recht, zu
verlangen, dass Sie betreffende Daten unverzüglich gelöscht werden,
bzw. alternativ nach Maßgabe der gesetzlichen Vorgaben eine
Einschränkung der Verarbeitung der Daten zu verlangen.
</li>
<li>
<strong>Recht auf Datenübertragbarkeit:</strong> Sie haben das Recht,
Sie betreffende Daten, die Sie uns bereitgestellt haben, nach Maßgabe
der gesetzlichen Vorgaben in einem strukturierten, gängigen und
maschinenlesbaren Format zu erhalten oder deren Übermittlung an einen
anderen Verantwortlichen zu fordern.
</li>
<li>
<strong>Beschwerde bei Aufsichtsbehörde:</strong> Sie haben
unbeschadet eines anderweitigen verwaltungsrechtlichen oder
gerichtlichen Rechtsbehelfs das Recht auf Beschwerde bei einer
Aufsichtsbehörde, insbesondere in dem Mitgliedstaat ihres gewöhnlichen
Aufenthaltsorts, ihres Arbeitsplatzes oder des Orts des mutmaßlichen
Verstoßes, wenn Sie der Ansicht sind, dass die Verarbeitung der Sie
betreffenden personenbezogenen Daten gegen die Vorgaben der DSGVO
verstößt.
</li>
</ul>
<h2 id="m42">Begriffsdefinitionen</h2>
<p>
In diesem Abschnitt erhalten Sie eine Übersicht über die in dieser
Datenschutzerklärung verwendeten Begrifflichkeiten. Viele der Begriffe
sind dem Gesetz entnommen und vor allem im Art. 4 DSGVO definiert. Die
gesetzlichen Definitionen sind verbindlich. Die nachfolgenden
Erläuterungen sollen dagegen vor allem dem Verständnis dienen. Die
Begriffe sind alphabetisch sortiert.
</p>
<ul class="glossary">
<li>
<strong>Personenbezogene Daten:</strong> "Personenbezogene Daten“ sind
alle Informationen, die sich auf eine identifizierte oder
identifizierbare natürliche Person (im Folgenden "betroffene Person)
beziehen; als identifizierbar wird eine natürliche Person angesehen,
die direkt oder indirekt, insbesondere mittels Zuordnung zu einer
Kennung wie einem Namen, zu einer Kennnummer, zu Standortdaten, zu
einer Online-Kennung (z.B. Cookie) oder zu einem oder mehreren
besonderen Merkmalen identifiziert werden kann, die Ausdruck der
physischen, physiologischen, genetischen, psychischen,
wirtschaftlichen, kulturellen oder sozialen Identität dieser
natürlichen Person sind.
</li>
<li>
<strong>Verantwortlicher:</strong> Als "Verantwortlicher“ wird die
natürliche oder juristische Person, Behörde, Einrichtung oder andere
Stelle, die allein oder gemeinsam mit anderen über die Zwecke und
Mittel der Verarbeitung von personenbezogenen Daten entscheidet,
bezeichnet.
</li>
<li>
<strong>Verarbeitung:</strong> "Verarbeitung" ist jeder mit oder ohne
Hilfe automatisierter Verfahren ausgeführte Vorgang oder jede solche
Vorgangsreihe im Zusammenhang mit personenbezogenen Daten. Der Begriff
reicht weit und umfasst praktisch jeden Umgang mit Daten, sei es das
Erheben, das Auswerten, das Speichern, das Übermitteln oder das
Löschen.
</li>
</ul>
<p class="seal">
<a
href="https://datenschutz-generator.de/"
rel="noopener noreferrer nofollow"
target="_blank"
title="Rechtstext von Dr. Schwenke - für weitere Informationen bitte anklicken."
>Erstellt mit kostenlosem Datenschutz-Generator.de von Dr. Thomas
Schwenke</a
>
</p>
</div>
</div>
</template>
<style scoped></style>

View File

@@ -3,8 +3,11 @@ import moduleStore from "../store/moduleStore.ts";
import { createIndividualFeed } from "../api/createFeed.ts";
import router from "../router";
import tokenStore from "../store/tokenStore.ts";
import { ref } from "vue";
import { computed, ref } from "vue";
import ModuleTemplateDialog from "./ModuleTemplateDialog.vue";
import { onlyWhitespace } from "../helpers/strings.ts";
import { useI18n } from "vue-i18n";
const { t } = useI18n({ useScope: "global" });
const store = moduleStore();
const tableData = ref(
@@ -16,10 +19,10 @@ const tableData = ref(
}),
);
const columns = ref([
{ field: "Course", header: "Course" },
{ field: "Module", header: "Module" },
{ field: "Reminder", header: "Reminder" },
const columns = computed(() => [
{ field: "Course", header: t("moduleInformation.course") },
{ field: "Module", header: t("moduleInformation.module") },
{ field: "Reminder", header: t("renameModules.reminder") },
]);
async function finalStep() {
@@ -32,7 +35,7 @@ async function finalStep() {
<template>
<div class="flex flex-column">
<div class="flex align-items-center justify-content-center h-4rem m-2">
<h3>Configure your selected Modules to your liking.</h3>
<h3>{{ $t("renameModules.subTitle") }}</h3>
<ModuleTemplateDialog />
</div>
<div class="card flex align-items-center justify-content-center m-2">
@@ -44,7 +47,7 @@ async function finalStep() {
>
<template #header>
<div class="flex align-items-center justify-content-end">
Enable all notifications:
{{ $t("renameModules.enableAllNotifications") }}
<InputSwitch
class="mx-4"
:model-value="
@@ -69,7 +72,11 @@ async function finalStep() {
<!-- Text Body -->
<template #body="{ data, field }">
<template v-if="field === 'Module'">
{{ data[field].userDefinedName }}
{{
onlyWhitespace(data[field].userDefinedName)
? data[field].name
: data[field].userDefinedName
}}
</template>
<template v-else-if="field === 'Reminder'">
<Button
@@ -95,10 +102,6 @@ async function finalStep() {
/>
</template>
<template v-else-if="field === 'Reminder'">
<!--<InputSwitch
v-model="data.Module.reminder"
class="align-self-center"
/>-->
<Button
:icon="data.Module.reminder ? 'pi pi-bell' : 'pi pi-times'"
:severity="data.Module.reminder ? 'warning' : 'secondary'"
@@ -117,7 +120,7 @@ async function finalStep() {
</div>
<div class="flex align-items-center justify-content-center h-4rem m-2">
<Button @click="finalStep()">Next Step</Button>
<Button @click="finalStep()">{{ $t("renameModules.nextStep") }}</Button>
</div>
</div>
</template>

View File

@@ -1,47 +0,0 @@
<script lang="ts" setup>
import { Ref, ref } from "vue";
import { fetchRoom } from "../api/fetchRoom.ts";
import RoomOccupation from "./RoomOccupation.vue";
const rooms = async () => {
return await fetchRoom();
};
const roomsList: Ref<{ name: string }[]> = ref([]);
const selectedRoom: Ref<{ name: string }> = ref({ name: "" });
rooms().then(
(data) =>
(roomsList.value = data.map((room) => {
return { name: room };
})),
);
</script>
<template>
<div class="flex flex-column">
<div class="flex align-items-center justify-content-center h-4rem m-2">
<h3 class="text-4xl">Raumfinder</h3>
</div>
<div
class="flex align-items-center justify-content-center h-4rem border-round m-2"
>
<h5 class="text-2xl">Please select a room</h5>
</div>
<div
class="flex align-items-center justify-content-center border-round m-2"
>
<Dropdown
v-model="selectedRoom"
:options="roomsList"
class="w-full md:w-25rem mx-2"
filter
option-label="name"
placeholder="Select a Room"
/>
</div>
<div class="m-6">
<RoomOccupation :room="selectedRoom.name" />
</div>
</div>
</template>

View File

@@ -3,9 +3,11 @@ import FullCalendar from "@fullcalendar/vue3";
import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin from "@fullcalendar/interaction";
import timeGridPlugin from "@fullcalendar/timegrid";
import { computed, ref, Ref, watch } from "vue";
import { computed, ComputedRef, ref, Ref, watch } from "vue";
import { CalendarOptions, EventInput } from "@fullcalendar/core";
import { fetchEventsByRoomAndDuration } from "../api/fetchRoom.ts";
import { useI18n } from "vue-i18n";
const { t } = useI18n({ useScope: "global" });
const props = defineProps({
room: {
@@ -53,84 +55,92 @@ async function getOccupation() {
calendar?.refetchEvents();
}
const calendarOptions: CalendarOptions = {
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin],
initialView: "week",
dayHeaderFormat: { weekday: "short", omitCommas: true },
slotDuration: "00:15:00",
eventTimeFormat: {
hour: "2-digit",
minute: "2-digit",
hour12: false,
},
views: {
week: {
type: "timeGrid",
slotLabelFormat: {
hour: "numeric",
minute: "2-digit",
omitZeroMinute: false,
meridiem: false,
hour12: false,
},
dateAlignment: "week",
titleFormat: { month: "short", day: "numeric" },
slotMinTime: "06:00:00",
slotMaxTime: "22:00:00",
import allLocales from "@fullcalendar/core/locales-all";
duration: { days: 7 },
firstDay: 1,
allDaySlot: false,
hiddenDays: [0],
const calendarOptions: ComputedRef<CalendarOptions> = computed(() => {
return {
locales: allLocales,
locale: t("languageCode"),
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin],
initialView: "week",
dayHeaderFormat: { weekday: "short", omitCommas: true },
slotDuration: "00:15:00",
eventTimeFormat: {
hour: "2-digit",
minute: "2-digit",
hour12: false,
},
Day: {
type: "timeGrid",
slotLabelFormat: {
hour: "numeric",
minute: "2-digit",
omitZeroMinute: false,
meridiem: false,
hour12: false,
views: {
week: {
description: "Wochenansicht",
type: "timeGrid",
slotLabelFormat: {
hour: "numeric",
minute: "2-digit",
omitZeroMinute: false,
meridiem: false,
hour12: false,
},
dateAlignment: "week",
titleFormat: { month: "short", day: "numeric" },
slotMinTime: "06:00:00",
slotMaxTime: "22:00:00",
duration: { days: 7 },
firstDay: 1,
allDaySlot: false,
hiddenDays: [0],
},
Day: {
type: "timeGrid",
slotLabelFormat: {
hour: "numeric",
minute: "2-digit",
omitZeroMinute: false,
meridiem: false,
hour12: false,
},
titleFormat: { month: "short", day: "numeric" },
slotMinTime: "06:00:00",
slotMaxTime: "22:00:00",
duration: { days: 1 },
allDaySlot: false,
hiddenDays: [0],
},
titleFormat: { month: "short", day: "numeric" },
slotMinTime: "06:00:00",
slotMaxTime: "22:00:00",
duration: { days: 1 },
allDaySlot: false,
hiddenDays: [0],
},
},
headerToolbar: {
end: "prev,next today",
center: "title",
start: "week,Day",
},
headerToolbar: {
end: "prev,next today",
center: "title",
start: "week,Day",
},
datesSet: function (dateInfo) {
const view = dateInfo.view;
const offset = new Date().getTimezoneOffset();
const startDate = new Date(view.activeStart.getTime() - offset * 60 * 1000);
const endDate = new Date(view.activeEnd.getTime() - offset * 60 * 1000);
currentDateFrom.value = startDate.toISOString().split("T")[0];
currentDateTo.value = endDate.toISOString().split("T")[0];
getOccupation();
},
events: function (_info, successCallback, failureCallback) {
if (occupations.value.length === 0) {
failureCallback(new Error("no events"));
} else {
successCallback(
occupations.value.map((event) => {
return {
id: event.id.toString(),
start: event.start,
end: event.end,
} as EventInput;
}),
datesSet: function (dateInfo: any) {
const view = dateInfo.view;
const offset = new Date().getTimezoneOffset();
const startDate = new Date(
view.activeStart.getTime() - offset * 60 * 1000,
);
}
},
};
const endDate = new Date(view.activeEnd.getTime() - offset * 60 * 1000);
currentDateFrom.value = startDate.toISOString().split("T")[0];
currentDateTo.value = endDate.toISOString().split("T")[0];
getOccupation();
},
events: function (_info: any, successCallback: any, failureCallback: any) {
if (occupations.value.length === 0) {
failureCallback(new Error("no events"));
} else {
successCallback(
occupations.value.map((event) => {
return {
id: event.id.toString(),
start: event.start,
end: event.end,
} as EventInput;
}),
);
}
},
};
});
</script>
<template>
<FullCalendar ref="fullCalendar" :options="calendarOptions" />

View File

@@ -91,6 +91,7 @@ function selectChange() {
class="custom-multiselect"
filter
placeholder="Select additional modules"
:auto-filter-focus="true"
@change="selectChange()"
@selectall-change="onSelectAllChange($event)"
>

View File

@@ -7,6 +7,7 @@ import { saveIndividualFeed } from "../../api/createFeed";
import tokenStore from "../../store/tokenStore";
import router from "../../router";
import ModuleTemplateDialog from "../ModuleTemplateDialog.vue";
import { onlyWhitespace } from "../../helpers/strings.ts";
const store = moduleStore();
const tableData = computed(() =>
@@ -90,7 +91,11 @@ async function finalStep() {
<!-- Text Body -->
<template #body="{ data, field }">
<template v-if="field === 'Module'">
{{ data[field].userDefinedName }}
{{
onlyWhitespace(data[field].userDefinedName)
? data[field].name
: data[field].userDefinedName
}}
</template>
<template v-else-if="field === 'Reminder'">
<Button
@@ -125,6 +130,9 @@ async function finalStep() {
@click="data.Module.reminder = !data.Module.reminder"
></Button>
</template>
<template v-else>
{{ data[field] }}
</template>
</template>
</Column>
<Column>