Merge branch '10-roomfinder' of github.com:masterElmar/htwkalender into 10-roomfinder

This commit is contained in:
Tom Wahl
2023-10-28 13:22:02 +02:00
50 changed files with 2432 additions and 498 deletions

6
.idea/jsLibraryMappings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

9
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/backend/backend.iml" filepath="$PROJECT_DIR$/backend/backend.iml" />
<module fileurl="file://$PROJECT_DIR$/frontend/frontend.iml" filepath="$PROJECT_DIR$/frontend/frontend.iml" />
</modules>
</component>
</project>

6
.idea/swagger-settings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SwaggerSettings">
<option name="defaultPreviewType" value="SWAGGER_UI" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

53
.idea/workspace.xml generated
View File

@@ -4,19 +4,8 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="81c34cb9-5538-45c5-b693-4f8a6bed15c4" name="Changes" comment=""> <list default="true" id="81c34cb9-5538-45c5-b693-4f8a6bed15c4" name="Changes" comment="added missing idea files">
<change afterPath="$PROJECT_DIR$/frontend/src/components/RoomFinder.vue" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/.gitignore" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/codeStyles/Project.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/codeStyles/codeStyleConfig.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/htwk-planner.iml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/modules.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/prettier.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/vcs.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/backend/go.mod" beforeDir="false" afterPath="$PROJECT_DIR$/backend/go.mod" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/go.sum" beforeDir="false" afterPath="$PROJECT_DIR$/backend/go.sum" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/main.go" beforeDir="false" afterPath="$PROJECT_DIR$/backend/main.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/router/index.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/router/index.ts" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -39,6 +28,13 @@
</option> </option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" /> <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component> </component>
<component name="GithubProjectSettings">
<option name="branchProtectionPatterns">
<list>
<option value="main" />
</list>
</option>
</component>
<component name="GoLibraries"> <component name="GoLibraries">
<option name="indexEntireGoPath" value="true" /> <option name="indexEntireGoPath" value="true" />
</component> </component>
@@ -59,6 +55,7 @@
&quot;associatedIndex&quot;: 2 &quot;associatedIndex&quot;: 2
}</component> }</component>
<component name="ProjectId" id="2WqbaFt9UED5KZtHQV9GfCdMxMd" /> <component name="ProjectId" id="2WqbaFt9UED5KZtHQV9GfCdMxMd" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
<component name="ProjectViewState"> <component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" /> <option name="showLibraryContents" value="true" />
@@ -73,15 +70,20 @@
"WebServerToolWindowFactoryState": "false", "WebServerToolWindowFactoryState": "false",
"git-widget-placeholder": "10-roomfinder", "git-widget-placeholder": "10-roomfinder",
"go.import.settings.migrated": "true", "go.import.settings.migrated": "true",
"last_opened_file_path": "/Users/core/ws/htwk/master/raumplan/htwkalender", "ignore.virus.scanning.warn.message": "true",
"jdk.selected.JAVA_MODULE": "20",
"last_opened_file_path": "C:/Users/masterelmar/GolandProjects/htwk-planner",
"node.js.detected.package.eslint": "true", "node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true", "node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)", "node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)", "node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm", "nodejs_package_manager_path": "npm",
"prettierjs.PrettierConfiguration.Package": "/Users/core/ws/htwk/master/raumplan/htwkalender/frontend/node_modules/prettier", "prettierjs.PrettierConfiguration.Package": "C:\\Users\\masterelmar\\GolandProjects\\htwk-planner\\frontend\\node_modules\\prettier",
"project.structure.last.edited": "Modules",
"project.structure.proportion": "0.0",
"project.structure.side.proportion": "0.42068964",
"settings.editor.selected.configurable": "com.goide.configuration.GoLibrariesConfigurableProvider", "settings.editor.selected.configurable": "com.goide.configuration.GoLibrariesConfigurableProvider",
"ts.external.directory.path": "/Users/core/ws/htwk/master/raumplan/htwkalender/frontend/node_modules/typescript/lib", "ts.external.directory.path": "C:\\Users\\masterelmar\\GolandProjects\\htwk-planner\\frontend\\node_modules\\typescript\\lib",
"vue.rearranger.settings.migration": "true" "vue.rearranger.settings.migration": "true"
} }
}]]></component> }]]></component>
@@ -131,12 +133,31 @@
<updated>1697463118186</updated> <updated>1697463118186</updated>
<workItem from="1697463119285" duration="2319000" /> <workItem from="1697463119285" duration="2319000" />
<workItem from="1697465553926" duration="3918000" /> <workItem from="1697465553926" duration="3918000" />
<workItem from="1697823319671" duration="31000" />
<workItem from="1697823365338" duration="611000" />
<workItem from="1697823990507" duration="315000" />
</task> </task>
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" /> <option name="version" value="3" />
</component> </component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
</map>
</option>
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="restored idea git detection" />
<MESSAGE value="added missing idea files" />
<option name="LAST_COMMIT_MESSAGE" value="added missing idea files" />
</component>
<component name="VgoProject"> <component name="VgoProject">
<integration-enabled>false</integration-enabled> <integration-enabled>false</integration-enabled>
<settings-migrated>true</settings-migrated> <settings-migrated>true</settings-migrated>

1
backend/.idea/.gitignore generated vendored
View File

@@ -6,3 +6,4 @@
# Datasource local storage ignored files # Datasource local storage ignored files
/dataSources/ /dataSources/
/dataSources.local.xml /dataSources.local.xml
/jsLibraryMappings.xml

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4"> <module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager" inherit-compiler-output="true"> <component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output /> <exclude-output />
<content url="file://$MODULE_DIR$" /> <content url="file://$MODULE_DIR$" />

View File

@@ -24,6 +24,8 @@ func main() {
service.AddRoutes(app) service.AddRoutes(app)
service.AddSchedules(app)
if err := app.Start(); err != nil { if err := app.Start(); err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@@ -0,0 +1,384 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models"
)
func init() {
m.Register(func(db dbx.Builder) error {
jsonData := `[
{
"id": "cfq9mqlmd97v8z5",
"created": "2023-09-19 17:31:15.957Z",
"updated": "2023-09-19 17:31:15.957Z",
"name": "groups",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "85msl21p",
"name": "university",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "2sii4dtp",
"name": "shortcut",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "uiwgo28f",
"name": "groupId",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "y0l1lrzs",
"name": "course",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "kr62mhbz",
"name": "faculty",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "ya6znpez",
"name": "facultyId",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_rcaN2Oq` + "`" + ` ON ` + "`" + `groups` + "`" + ` (` + "`" + `course` + "`" + `)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "d65h4wh7zk13gxp",
"created": "2023-09-19 17:31:15.957Z",
"updated": "2023-10-17 08:37:17.943Z",
"name": "feeds",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "cowxjfmc",
"name": "modules",
"type": "json",
"required": true,
"unique": false,
"options": {}
}
],
"indexes": [],
"listRule": null,
"viewRule": "",
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "7her4515qsmrxe8",
"created": "2023-09-19 17:31:15.958Z",
"updated": "2023-09-19 17:31:15.958Z",
"name": "events",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "m8ne8e3m",
"name": "Day",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "xnsxqp7j",
"name": "Week",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "7vsr9h6p",
"name": "Start",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "wwpokofe",
"name": "End",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "aeuskrjo",
"name": "Name",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "klrzqyw0",
"name": "EventType",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "5zltexoy",
"name": "Prof",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "gy3nvfmx",
"name": "Rooms",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "hn7b8dfy",
"name": "Notes",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "axskpwm8",
"name": "BookedAt",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "vyyefxp7",
"name": "course",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "vlbpm9fz",
"name": "semester",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_orp1NWL` + "`" + ` ON ` + "`" + `events` + "`" + ` (\n ` + "`" + `Day` + "`" + `,\n ` + "`" + `Week` + "`" + `,\n ` + "`" + `Start` + "`" + `,\n ` + "`" + `End` + "`" + `,\n ` + "`" + `Name` + "`" + `,\n ` + "`" + `course` + "`" + `,\n ` + "`" + `Prof` + "`" + `,\n ` + "`" + `Rooms` + "`" + `,\n ` + "`" + `EventType` + "`" + `\n)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "_pb_users_auth_",
"created": "2023-10-08 16:32:34.131Z",
"updated": "2023-10-08 16:32:34.315Z",
"name": "users",
"type": "auth",
"system": false,
"schema": [
{
"system": false,
"id": "users_name",
"name": "name",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "users_avatar",
"name": "avatar",
"type": "file",
"required": false,
"unique": false,
"options": {
"maxSelect": 1,
"maxSize": 5242880,
"mimeTypes": [
"image/jpeg",
"image/png",
"image/svg+xml",
"image/gif",
"image/webp"
],
"thumbs": null,
"protected": false
}
}
],
"indexes": [],
"listRule": "id = @request.auth.id",
"viewRule": "id = @request.auth.id",
"createRule": "",
"updateRule": "id = @request.auth.id",
"deleteRule": "id = @request.auth.id",
"options": {
"allowEmailAuth": true,
"allowOAuth2Auth": true,
"allowUsernameAuth": true,
"exceptEmailDomains": null,
"manageRule": null,
"minPasswordLength": 8,
"onlyEmailDomains": null,
"requireEmail": false
}
}
]`
collections := []*models.Collection{}
if err := json.Unmarshal([]byte(jsonData), &collections); err != nil {
return err
}
return daos.New(db).ImportCollections(collections, true, nil)
}, func(db dbx.Builder) error {
return nil
})
}

View File

@@ -0,0 +1,384 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models"
)
func init() {
m.Register(func(db dbx.Builder) error {
jsonData := `[
{
"id": "cfq9mqlmd97v8z5",
"created": "2023-09-19 17:31:15.957Z",
"updated": "2023-10-17 10:50:08.270Z",
"name": "groups",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "85msl21p",
"name": "university",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "2sii4dtp",
"name": "shortcut",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "uiwgo28f",
"name": "groupId",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "y0l1lrzs",
"name": "course",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "kr62mhbz",
"name": "faculty",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "ya6znpez",
"name": "facultyId",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_rcaN2Oq` + "`" + ` ON ` + "`" + `groups` + "`" + ` (` + "`" + `course` + "`" + `)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "d65h4wh7zk13gxp",
"created": "2023-09-19 17:31:15.957Z",
"updated": "2023-10-17 18:47:10.221Z",
"name": "feeds",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "cowxjfmc",
"name": "modules",
"type": "json",
"required": true,
"unique": false,
"options": {}
}
],
"indexes": [],
"listRule": null,
"viewRule": "",
"createRule": null,
"updateRule": "",
"deleteRule": null,
"options": {}
},
{
"id": "7her4515qsmrxe8",
"created": "2023-09-19 17:31:15.958Z",
"updated": "2023-10-17 10:50:08.270Z",
"name": "events",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "m8ne8e3m",
"name": "Day",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "xnsxqp7j",
"name": "Week",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "7vsr9h6p",
"name": "Start",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "wwpokofe",
"name": "End",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "aeuskrjo",
"name": "Name",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "klrzqyw0",
"name": "EventType",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "5zltexoy",
"name": "Prof",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "gy3nvfmx",
"name": "Rooms",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "hn7b8dfy",
"name": "Notes",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "axskpwm8",
"name": "BookedAt",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "vyyefxp7",
"name": "course",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "vlbpm9fz",
"name": "semester",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
}
],
"indexes": [
"CREATE UNIQUE INDEX ` + "`" + `idx_orp1NWL` + "`" + ` ON ` + "`" + `events` + "`" + ` (\n ` + "`" + `Day` + "`" + `,\n ` + "`" + `Week` + "`" + `,\n ` + "`" + `Start` + "`" + `,\n ` + "`" + `End` + "`" + `,\n ` + "`" + `Name` + "`" + `,\n ` + "`" + `course` + "`" + `,\n ` + "`" + `Prof` + "`" + `,\n ` + "`" + `Rooms` + "`" + `,\n ` + "`" + `EventType` + "`" + `\n)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "_pb_users_auth_",
"created": "2023-09-22 09:31:11.498Z",
"updated": "2023-10-17 10:50:08.270Z",
"name": "users",
"type": "auth",
"system": false,
"schema": [
{
"system": false,
"id": "users_name",
"name": "name",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "users_avatar",
"name": "avatar",
"type": "file",
"required": false,
"unique": false,
"options": {
"maxSelect": 1,
"maxSize": 5242880,
"mimeTypes": [
"image/jpeg",
"image/png",
"image/svg+xml",
"image/gif",
"image/webp"
],
"thumbs": null,
"protected": false
}
}
],
"indexes": [],
"listRule": "id = @request.auth.id",
"viewRule": "id = @request.auth.id",
"createRule": "",
"updateRule": "id = @request.auth.id",
"deleteRule": "id = @request.auth.id",
"options": {
"allowEmailAuth": true,
"allowOAuth2Auth": true,
"allowUsernameAuth": true,
"exceptEmailDomains": null,
"manageRule": null,
"minPasswordLength": 8,
"onlyEmailDomains": null,
"requireEmail": false
}
}
]`
collections := []*models.Collection{}
if err := json.Unmarshal([]byte(jsonData), &collections); err != nil {
return err
}
return daos.New(db).ImportCollections(collections, true, nil)
}, func(db dbx.Builder) error {
return nil
})
}

View File

@@ -1,3 +1,18 @@
package model package model
type Events []*Event type Events []*Event
type Event struct {
Day string `db:"Day" json:"day"`
Week string `db:"Week" json:"week"`
Start string `db:"Start" json:"start"`
End string `db:"End" json:"end"`
Name string `db:"Name" json:"name"`
EventType string `db:"EventType" json:"eventType"`
Prof string `db:"Prof" json:"prof"`
Rooms string `db:"Rooms" json:"rooms"`
Notes string `db:"Notes" json:"notes"`
BookedAt string `db:"BookedAt" json:"bookedAt"`
Course string `db:"course" json:"course"`
Semester string `db:"semester" json:"semester"`
}

View File

@@ -23,7 +23,7 @@ type Entry struct {
type Entries []*Entry type Entries []*Entry
type FeedCollection struct { type FeedCollection struct {
Name string `db:"Name" json:"Name"` Name string `db:"Name" json:"name"`
Course string `db:"course" json:"Course"` Course string `db:"course" json:"course"`
UserDefinedName string `db:"userDefinedName" json:"UserDefinedName"` UserDefinedName string `db:"userDefinedName" json:"userDefinedName"`
} }

View File

@@ -0,0 +1,9 @@
package model
type Module struct {
Name string `json:"name"`
Prof string `json:"prof"`
Course string `json:"course"`
Semester string `json:"semester"`
Events Events `json:"events"`
}

View File

@@ -9,18 +9,3 @@ type SeminarGroup struct {
FacultyId string FacultyId string
Events []Event Events []Event
} }
type Event struct {
Day string `db:"course"`
Week string `db:"Week"`
Start string `db:"Start"`
End string `db:"End"`
Name string `db:"Name"`
EventType string `db:"EventType"`
Prof string `db:"Prof"`
Rooms string `db:"Rooms"`
Notes string `db:"Notes"`
BookedAt string `db:"BookedAt"`
Course string `db:"course"`
Semester string `db:"semester"`
}

View File

@@ -1,7 +1,12 @@
xopenapi: 3.0.0 xopenapi: 3.0.0
info: info:
title: Your API title: HTWKalendar API
version: 1.0.0 version: 1.0.1
servers:
- url: https://cal.ekresse.de
description: Production server
- url: http://localhost:8090
description: Local server
paths: paths:
/api/fetchPlans: /api/fetchPlans:
get: get:
@@ -29,7 +34,20 @@ paths:
description: Successful response description: Successful response
/api/feed: /api/feed:
get: get:
summary: Get iCal Feed summary: Get iCal Feed for calendar
responses:
'200':
description: Successful response
/api/collections/feeds/records/{id}:
get:
summary: Get Modules selected for iCal feed
parameters:
- name: id
in: path
description: calendar token
required: true
schema:
type: string
responses: responses:
'200': '200':
description: Successful response description: Successful response

View File

@@ -9,7 +9,9 @@ import (
"htwkalender/service/fetch" "htwkalender/service/fetch"
"htwkalender/service/ical" "htwkalender/service/ical"
"htwkalender/service/room" "htwkalender/service/room"
"io"
"net/http" "net/http"
"net/url"
"os" "os"
) )
@@ -71,6 +73,7 @@ func AddRoutes(app *pocketbase.PocketBase) {
return nil return nil
}) })
// API Endpoint to get all events for a specific room on a specific day
app.OnBeforeServe().Add(func(e *core.ServeEvent) error { app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
_, err := e.Router.AddRoute(echo.Route{ _, err := e.Router.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
@@ -90,6 +93,7 @@ func AddRoutes(app *pocketbase.PocketBase) {
return nil return nil
}) })
// API Endpoint to create a new iCal feed
app.OnBeforeServe().Add(func(e *core.ServeEvent) error { app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
_, err := e.Router.AddRoute(echo.Route{ _, err := e.Router.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
@@ -115,7 +119,13 @@ func AddRoutes(app *pocketbase.PocketBase) {
Method: http.MethodPost, Method: http.MethodPost,
Path: "/api/createFeed", Path: "/api/createFeed",
Handler: func(c echo.Context) error { Handler: func(c echo.Context) error {
return ical.CreateIndividualFeed(c, app) requestBody, _ := io.ReadAll(c.Request().Body)
result, err := ical.CreateIndividualFeed(requestBody, app)
if err != nil {
return c.JSON(http.StatusInternalServerError, err)
}
return c.JSON(http.StatusOK, result)
}, },
Middlewares: []echo.MiddlewareFunc{ Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app), apis.ActivityLogger(app),
@@ -152,7 +162,13 @@ func AddRoutes(app *pocketbase.PocketBase) {
Handler: func(c echo.Context) error { Handler: func(c echo.Context) error {
course := c.QueryParam("course") course := c.QueryParam("course")
semester := c.QueryParam("semester") semester := c.QueryParam("semester")
return events.GetModulesForCourseDistinct(app, c, course, semester) modules, err := events.GetModulesForCourseDistinct(app, course, semester)
if err != nil {
return c.JSON(400, err)
} else {
return c.JSON(200, modules)
}
}, },
Middlewares: []echo.MiddlewareFunc{ Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app), apis.ActivityLogger(app),
@@ -181,12 +197,62 @@ func AddRoutes(app *pocketbase.PocketBase) {
return nil return nil
}) })
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
_, err := e.Router.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/api/module",
Handler: func(c echo.Context) error {
name := c.Request().Header.Get("Name")
name, err := url.QueryUnescape(name)
module, err := events.GetModuleByName(app, name)
if err != nil {
return c.JSON(400, err)
} else {
return c.JSON(200, module)
}
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
},
})
if err != nil {
return err
}
return nil
})
app.OnBeforeServe().Add(func(e *core.ServeEvent) error { app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
_, err := e.Router.AddRoute(echo.Route{ _, err := e.Router.AddRoute(echo.Route{
Method: http.MethodGet, Method: http.MethodGet,
Path: "/api/courses", Path: "/api/courses",
Handler: func(c echo.Context) error { Handler: func(c echo.Context) error {
return events.GetAllCourses(app, c) courses := events.GetAllCourses(app)
return c.JSON(200, courses)
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
},
})
if err != nil {
return err
}
return nil
})
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
_, err := e.Router.AddRoute(echo.Route{
Method: http.MethodDelete,
Path: "/api/events",
Handler: func(c echo.Context) error {
course := c.QueryParam("course")
semester := c.QueryParam("semester")
err := events.DeleteAllEventsByCourseAndSemester(app, course, semester)
if err != nil {
return c.JSON(400, err)
} else {
return c.JSON(200, "Events deleted")
}
}, },
Middlewares: []echo.MiddlewareFunc{ Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app), apis.ActivityLogger(app),

View File

@@ -0,0 +1,40 @@
package service
import (
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/cron"
"htwkalender/service/events"
"log"
)
func AddSchedules(app *pocketbase.PocketBase) {
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
scheduler := cron.New()
// Every hour update all courses (5 segments - minute, hour, day, month, weekday) "0 * * * *"
// Every three hours update all courses (5 segments - minute, hour, day, month, weekday) "0 */3 * * *"
// Every 10 minutes update all courses (5 segments - minute, hour, day, month, weekday) "*/10 * * * *"
scheduler.MustAdd("updateCourse", "0 */3 * * *", func() {
courses := events.GetAllCourses(app)
for _, course := range courses {
err := events.UpdateModulesForCourse(app, course)
if err != nil {
log.Println("Update Course: " + course + " failed")
log.Println(err)
} else {
log.Println("Update Course: " + course + " successful")
}
}
})
scheduler.Start()
return nil
})
}

View File

@@ -77,74 +77,113 @@ func findEventByDayWeekStartEndNameCourse(event model.Event, course string, app
return &event, err return &event, err
} }
func buildIcalQueryForModules(modules []model.FeedCollection) dbx.Expression {
// build where conditions for each module
//first check if modules is empty
if len(modules) == 0 {
return dbx.HashExp{}
}
//second check if modules has only one element
if len(modules) == 1 {
return dbx.And(
dbx.HashExp{"Name": modules[0].Name},
dbx.HashExp{"course": modules[0].Course},
)
}
//third check if modules has more than one element
var wheres []dbx.Expression
for _, module := range modules {
where := dbx.And(
dbx.HashExp{"Name": module.Name},
dbx.HashExp{"course": module.Course},
)
wheres = append(wheres, where)
}
// Use dbx.And or dbx.Or to combine the where conditions as needed
where := dbx.Or(wheres...)
return where
}
// GetPlanForModules returns all events for the given modules with the given course
// used for the ical feed
func GetPlanForModules(app *pocketbase.PocketBase, modules []model.FeedCollection) model.Events { func GetPlanForModules(app *pocketbase.PocketBase, modules []model.FeedCollection) model.Events {
// build query functions with name equals elements in modules for dbx query
var queryString string
for i, module := range modules {
if i == 0 {
queryString = "Name = '" + module.Name + "' AND course = '" + module.Course + "'"
} else {
queryString = queryString + " OR Name = '" + module.Name + "' AND course = '" + module.Course + "'"
}
}
var events model.Events var events model.Events
// iterate over modules in 100 batch sizes
for i := 0; i < len(modules); i += 100 {
var moduleBatch []model.FeedCollection
if i+100 > len(modules) {
moduleBatch = modules[i:]
} else {
moduleBatch = modules[i : i+100]
}
var selectedModulesQuery = buildIcalQueryForModules(moduleBatch)
// get all events from event records in the events collection // get all events from event records in the events collection
err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp(queryString)).All(&events) err := app.Dao().DB().Select("*").From("events").Where(selectedModulesQuery).All(&events)
if err != nil { if err != nil {
print("Error while getting events from database: ", err) print("Error while getting events from database: ", err)
return nil return nil
} }
}
return events return events
} }
func GetAllModulesForCourse(app *pocketbase.PocketBase, course string, semester string) ([]string, error) { func GetAllModulesForCourse(app *pocketbase.PocketBase, course string, semester string) (model.Events, error) {
var events []struct { var events model.Events
Name string `db:"Name" json:"Name"`
}
var eventArray []string
// get all events from event records in the events collection // get all events from event records in the events collection
err := app.Dao().DB().Select("Name").From("events").Where(dbx.NewExp("course = {:course} AND semester = {:semester}", dbx.Params{"course": course, "semester": semester})).Distinct(true).All(&events) err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("course = {:course} AND semester = {:semester}", dbx.Params{"course": course, "semester": semester})).GroupBy("Name").Distinct(true).All(&events)
if err != nil { if err != nil {
print("Error while getting events from database: ", err) print("Error while getting events from database: ", err)
return eventArray, err return nil, err
} }
for _, event := range events { return events, nil
eventArray = append(eventArray, event.Name)
}
return eventArray, nil
} }
func GetAllModulesDistinct(app *pocketbase.PocketBase) ([]struct { func GetAllModulesDistinctByNameAndCourse(app *pocketbase.PocketBase) (model.Events, error) {
Name string var events model.Events
Course string
}, error) {
var events []struct {
Name string `db:"Name" json:"Name"`
Course string `db:"course" json:"course"`
}
var eventArray []struct { err := app.Dao().DB().Select("*").From("events").GroupBy("Name", "course").Distinct(true).All(&events)
Name string
Course string
}
err := app.Dao().DB().Select("Name", "course").From("events").Distinct(true).All(&events)
if err != nil { if err != nil {
print("Error while getting events from database: ", err) print("Error while getting events from database: ", err)
return eventArray, err return nil, err
} }
for _, event := range events { return events, nil
eventArray = append(eventArray, struct { }
Name string
Course string func DeleteAllEventsForCourse(app *pocketbase.PocketBase, course string, semester string) error {
}{event.Name, event.Course}) _, err := app.Dao().DB().Delete("events", dbx.NewExp("course = {:course} AND semester = {:semester}", dbx.Params{"course": course, "semester": semester})).Execute()
}
return eventArray, nil if err != nil {
print("Error while deleting events from database: ", err)
return err
}
return nil
}
func FindAllEventsByModule(app *pocketbase.PocketBase, moduleName string) (model.Events, error) {
var events model.Events
err := app.Dao().DB().Select("*").From("events").Where(dbx.NewExp("Name = {:moduleName}", dbx.Params{"moduleName": moduleName})).All(&events)
if err != nil {
print("Error while getting events from database: ", err)
return nil, err
}
return events, nil
} }

View File

@@ -0,0 +1,42 @@
package db
import (
"github.com/pocketbase/dbx"
"htwkalender/model"
"reflect"
"testing"
)
func Test_buildIcalQueryForModules(t *testing.T) {
type args struct {
modules []model.FeedCollection
}
tests := []struct {
name string
args args
want dbx.Expression
}{
{
name: "empty modules",
args: args{modules: []model.FeedCollection{}},
want: dbx.HashExp{},
},
{
name: "one module",
args: args{modules: []model.FeedCollection{{Name: "test", Course: "test"}}},
want: dbx.And(dbx.HashExp{"Name": "test"}, dbx.HashExp{"course": "test"}),
},
{
name: "two modules",
args: args{modules: []model.FeedCollection{{Name: "test", Course: "test"}, {Name: "test2", Course: "test2"}}},
want: dbx.Or(dbx.And(dbx.HashExp{"Name": "test"}, dbx.HashExp{"course": "test"}), dbx.And(dbx.HashExp{"Name": "test2"}, dbx.HashExp{"course": "test2"})),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := buildIcalQueryForModules(tt.args.modules); !reflect.DeepEqual(got, tt.want) {
t.Errorf("buildIcalQueryForModules() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -1,12 +1,10 @@
package events package events
import ( import (
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase" "github.com/pocketbase/pocketbase"
"htwkalender/service/db" "htwkalender/service/db"
) )
func GetAllCourses(app *pocketbase.PocketBase, c echo.Context) error { func GetAllCourses(app *pocketbase.PocketBase) []string {
courses := db.GetAllCourses(app) return db.GetAllCourses(app)
return c.JSON(200, courses)
} }

View File

@@ -3,36 +3,24 @@ package events
import ( import (
"github.com/labstack/echo/v5" "github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase" "github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
"htwkalender/model"
"htwkalender/service/db" "htwkalender/service/db"
"htwkalender/service/fetch"
"htwkalender/service/functions" "htwkalender/service/functions"
) )
func GetModulesForCourseDistinct(app *pocketbase.PocketBase, c echo.Context, course string, semester string) error { func GetModulesForCourseDistinct(app *pocketbase.PocketBase, course string, semester string) (model.Events, error) {
modules, err := db.GetAllModulesForCourse(app, course, semester) modules, err := db.GetAllModulesForCourse(app, course, semester)
replaceEmptyEntryInStringArray(modules, "Sonderveranstaltungen") replaceEmptyEntry(modules, "Sonderveranstaltungen")
return modules, err
if err != nil {
return c.JSON(400, err)
} else {
return c.JSON(200, modules)
}
} }
func replaceEmptyEntryInStringArray(modules []string, replacement string) { // replaceEmptyEntry replaces an empty entry in a module with a replacement string
//replace empty functions with "Sonderveranstaltungen" // If the module is not empty, nothing happens
for i, module := range modules { func replaceEmptyEntry(modules model.Events, replacement string) {
if functions.OnlyWhitespace(module) {
modules[i] = replacement
}
}
}
func replaceEmptyEntry(modules []struct {
Name string
Course string
}, replacement string) {
//replace empty functions with "Sonderveranstaltungen"
for i, module := range modules { for i, module := range modules {
if functions.OnlyWhitespace(module.Name) { if functions.OnlyWhitespace(module.Name) {
modules[i].Name = replacement modules[i].Name = replacement
@@ -40,8 +28,10 @@ func replaceEmptyEntry(modules []struct {
} }
} }
// GetAllModulesDistinct returns all modules distinct by name and course from the database
// That means you get all modules with duplicates if they have different courses
func GetAllModulesDistinct(app *pocketbase.PocketBase, c echo.Context) error { func GetAllModulesDistinct(app *pocketbase.PocketBase, c echo.Context) error {
modules, err := db.GetAllModulesDistinct(app) modules, err := db.GetAllModulesDistinctByNameAndCourse(app)
replaceEmptyEntry(modules, "Sonderveranstaltungen") replaceEmptyEntry(modules, "Sonderveranstaltungen")
@@ -51,3 +41,134 @@ func GetAllModulesDistinct(app *pocketbase.PocketBase, c echo.Context) error {
return c.JSON(200, modules) return c.JSON(200, modules)
} }
} }
// GetModuleByName returns a module by its name
// If the module does not exist, an error is returned
// If the module exists, the module is returned
// Module is a struct that exists in database as events
func GetModuleByName(app *pocketbase.PocketBase, name string) (model.Module, error) {
events, err := db.FindAllEventsByModule(app, name)
if err != nil || len(events) == 0 {
return model.Module{}, err
} else {
return model.Module{
Name: name,
Events: events,
Prof: events[0].Prof,
Course: events[0].Course,
Semester: events[0].Semester,
}, nil
}
}
// DeleteAllEventsByCourseAndSemester deletes all events for a course and a semester
// If the deletion was successful, nil is returned
// If the deletion was not successful, an error is returned
func DeleteAllEventsByCourseAndSemester(app *pocketbase.PocketBase, course string, semester string) error {
err := db.DeleteAllEventsForCourse(app, course, semester)
if err != nil {
return err
} else {
return nil
}
}
// UpdateModulesForCourse updates all modules for a course
// Does Updates for ws and ss semester sequentially
// Update runs through the following steps:
// 1. Delete all events for the course and the semester
// 2. Fetch all events for the course and the semester
// 3. Save all events for the course and the semester
// If the update was successful, nil is returned
// If the update was not successful, an error is returned
func UpdateModulesForCourse(app *pocketbase.PocketBase, course string) error {
//new string array with one element (course)
var courses []string
courses = append(courses, course)
seminarGroups := fetch.GetSeminarGroupsEventsFromHTML(courses)
collection, dbError := db.FindCollection(app, "events")
if dbError != nil {
return apis.NewNotFoundError("Collection not found", dbError)
}
seminarGroups = fetch.ClearEmptySeminarGroups(seminarGroups)
seminarGroups = fetch.ReplaceEmptyEventNames(seminarGroups)
//check if events in the seminarGroups Events are already in the database
//if yes, keep the database as it is
//if no, delete all events for the course and the semester and save the new events
//if there are no events in the database, save the new events
//get all events for the course and the semester
events, err := db.GetAllModulesForCourse(app, course, "ws")
if err != nil {
return apis.NewNotFoundError("Events for winter semester could not be found", err)
}
// append all events for the course and the semester to the events array for ss
summerEvents, err := db.GetAllModulesForCourse(app, course, "ss")
if err != nil {
return apis.NewNotFoundError("Events for summer semester could not be found", err)
}
events = append(events, summerEvents...)
//if there are no events in the database, save the new events
if len(events) == 0 {
_, dbError = db.SaveEvents(seminarGroups, collection, app)
if dbError != nil {
return apis.NewNotFoundError("Events could not be saved", dbError)
}
return nil
}
//check if events in the seminarGroups Events are already in the database
//if yes, keep the database as it is
//if no, delete all events for the course and the semester and save the new events
for _, seminarGroup := range seminarGroups {
for _, event := range seminarGroup.Events {
// if the event is not in the database, delete all events for the course and the semester and save the new events
if !ContainsEvent(events, event) {
err = DeleteAllEventsByCourseAndSemester(app, course, "ws")
if err != nil {
return err
}
err = DeleteAllEventsByCourseAndSemester(app, course, "ss")
if err != nil {
return err
}
//save the new events
_, dbError = db.SaveEvents(seminarGroups, collection, app)
if dbError != nil {
return apis.NewNotFoundError("Events could not be saved", dbError)
}
return nil
}
}
}
return nil
}
func ContainsEvent(events model.Events, event model.Event) bool {
for _, e := range events {
if e.Name == event.Name &&
e.Prof == event.Prof &&
e.Rooms == event.Rooms &&
e.Semester == event.Semester &&
e.Start == event.Start &&
e.End == event.End &&
e.Course == event.Course {
return true
}
}
return false
}

View File

@@ -28,7 +28,9 @@ func GetSeminarEvents(c echo.Context, app *pocketbase.PocketBase) error {
return apis.NewNotFoundError("Collection not found", dbError) return apis.NewNotFoundError("Collection not found", dbError)
} }
seminarGroups = clearEmptySeminarGroups(seminarGroups) seminarGroups = ClearEmptySeminarGroups(seminarGroups)
seminarGroups = ReplaceEmptyEventNames(seminarGroups)
savedRecords, dbError := db.SaveEvents(seminarGroups, collection, app) savedRecords, dbError := db.SaveEvents(seminarGroups, collection, app)
@@ -39,7 +41,18 @@ func GetSeminarEvents(c echo.Context, app *pocketbase.PocketBase) error {
return c.JSON(http.StatusOK, savedRecords) return c.JSON(http.StatusOK, savedRecords)
} }
func clearEmptySeminarGroups(seminarGroups []model.SeminarGroup) []model.SeminarGroup { func ReplaceEmptyEventNames(groups []model.SeminarGroup) []model.SeminarGroup {
for i, group := range groups {
for j, event := range group.Events {
if event.Name == "" {
groups[i].Events[j].Name = "Sonderveranstaltungen"
}
}
}
return groups
}
func ClearEmptySeminarGroups(seminarGroups []model.SeminarGroup) []model.SeminarGroup {
var newSeminarGroups []model.SeminarGroup var newSeminarGroups []model.SeminarGroup
for _, seminarGroup := range seminarGroups { for _, seminarGroup := range seminarGroups {
if len(seminarGroup.Events) > 0 && seminarGroup.Course != "" { if len(seminarGroup.Events) > 0 && seminarGroup.Course != "" {

View File

@@ -1,6 +1,10 @@
package fetch package fetch
import "testing" import (
"htwkalender/model"
"reflect"
"testing"
)
func Test_extractSemesterAndYear(t *testing.T) { func Test_extractSemesterAndYear(t *testing.T) {
type args struct { type args struct {
@@ -12,7 +16,6 @@ func Test_extractSemesterAndYear(t *testing.T) {
want string want string
want1 string want1 string
}{ }{
// TODO: Add test cases.
{ {
name: "Test 1", name: "Test 1",
args: args{ args: args{
@@ -50,3 +53,68 @@ func Test_extractSemesterAndYear(t *testing.T) {
}) })
} }
} }
func Test_replaceEmptyEventNames(t *testing.T) {
type args struct {
groups []model.SeminarGroup
}
tests := []struct {
name string
args args
want []model.SeminarGroup
}{
{
name: "Test 1",
args: args{
groups: []model.SeminarGroup{
{
Events: []model.Event{
{
Name: "Test",
},
},
},
},
},
want: []model.SeminarGroup{
{
Events: []model.Event{
{
Name: "Test",
},
},
},
},
},
{
name: "Test 1",
args: args{
groups: []model.SeminarGroup{
{
Events: []model.Event{
{
Name: "",
},
},
},
},
},
want: []model.SeminarGroup{
{
Events: []model.Event{
{
Name: "Sonderveranstaltungen",
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ReplaceEmptyEventNames(tt.args.groups); !reflect.DeepEqual(got, tt.want) {
t.Errorf("ReplaceEmptyEventNames() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -20,3 +20,10 @@ func Contains(s []string, e string) bool {
} }
return false return false
} }
func ReplaceEmptyString(word string, replacement string) string {
if OnlyWhitespace(word) {
return replacement
}
return word
}

View File

@@ -9,7 +9,6 @@ import (
"github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/apis"
"htwkalender/model" "htwkalender/model"
"htwkalender/service/db" "htwkalender/service/db"
"io"
"net/http" "net/http"
"time" "time"
) )
@@ -63,18 +62,12 @@ func writeSuccess(message string, w http.ResponseWriter) {
} }
} }
func CreateIndividualFeed(c echo.Context, app *pocketbase.PocketBase) error { func CreateIndividualFeed(requestBody []byte, app *pocketbase.PocketBase) (string, error) {
// read json from request body
var modules []model.FeedCollection var modules []model.FeedCollection
requestBodyBytes, err := io.ReadAll(c.Request().Body)
if err != nil {
return apis.NewApiError(400, "Could not bind request body", err)
}
err = json.Unmarshal(requestBodyBytes, &modules) err := json.Unmarshal(requestBody, &modules)
if err != nil { if err != nil {
return apis.NewApiError(400, "Could not bind request body", err) return "", apis.NewNotFoundError("Could not parse request body", err)
} }
var feed model.Feed var feed model.Feed
@@ -83,13 +76,13 @@ func CreateIndividualFeed(c echo.Context, app *pocketbase.PocketBase) error {
collection, dbError := db.FindCollection(app, "feeds") collection, dbError := db.FindCollection(app, "feeds")
if dbError != nil { if dbError != nil {
return apis.NewNotFoundError("Collection not found", dbError) return "", apis.NewNotFoundError("Collection could not be found", dbError)
} }
record, err := db.SaveFeed(feed, collection, app) record, err := db.SaveFeed(feed, collection, app)
if err != nil { if err != nil {
return apis.NewNotFoundError("Feed could not be saved", dbError) return "", apis.NewNotFoundError("Could not save feed", err)
} }
return c.JSON(http.StatusOK, record.Id) return record.Id, nil
} }

View File

@@ -62,6 +62,9 @@ func generateDescription(event *model.Event) string {
if !functions.OnlyWhitespace(event.Course) { if !functions.OnlyWhitespace(event.Course) {
description += "Gruppe: " + event.Course + "\n" description += "Gruppe: " + event.Course + "\n"
} }
if !functions.OnlyWhitespace(event.EventType) {
description += "Typ: " + event.EventType + "\n"
}
return description return description
} }

View File

@@ -18,3 +18,23 @@ export async function createIndividualFeed(modules: Module[]): Promise<string> {
}); });
return token; return token;
} }
export async function saveIndividualFeed(
token: string,
modules: Module[],
): Promise<string> {
await fetch("/api/collections/feeds/records/" + token, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: '{"modules":' + JSON.stringify(modules) + "}",
})
.then((response) => {
return response.json();
})
.then((response) => {
token = response;
});
return token;
}

View File

@@ -24,8 +24,17 @@ export async function fetchModulesByCourseAndSemester(
return response.json(); return response.json();
}) })
.then((modulesResponse) => { .then((modulesResponse) => {
modulesResponse.forEach((module: string) => modulesResponse.forEach((module: Module) =>
modules.push(new Module(module, course, module)), modules.push(
new Module(
module.name,
course,
module.name,
module.prof,
semester,
module.events,
),
),
); );
}); });
return modules; return modules;
@@ -39,7 +48,16 @@ export async function fetchAllModules(): Promise<Module[]> {
}) })
.then((responseModules: Module[]) => { .then((responseModules: Module[]) => {
responseModules.forEach((module: Module) => { responseModules.forEach((module: Module) => {
modules.push(new Module(module.Name, module.Course, module.Name)); modules.push(
new Module(
module.name,
module.course,
module.name,
module.prof,
module.semester,
module.events,
),
);
}); });
}); });

View File

@@ -0,0 +1,27 @@
import { Module } from "../model/module";
export async function fetchModule(name: string): Promise<Module> {
const request = new Request("/api/module", {
method: "GET",
headers: {
"Content-Type": "application/json",
Name: encodeURI(name),
},
});
return await fetch(request)
.then((response) => {
return response.json();
})
.then(
(module: Module) =>
new Module(
module.name,
module.course,
module.name,
module.prof,
module.semester,
module.events,
),
);
}

View File

@@ -0,0 +1,14 @@
import { Module } from "../model/module";
import { Calendar } from "../model/calendar";
export async function getCalender(token: string): Promise<Module[]> {
const request = new Request("/api/collections/feeds/records/" + token, {
method: "GET",
});
return await fetch(request)
.then((response) => {
return response.json();
})
.then((calendarResponse: Calendar) => calendarResponse.modules);
}

View File

@@ -1,11 +1,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, Ref } from "vue"; import { defineAsyncComponent, ref, Ref } from "vue";
import { Module } from "../model/module.ts"; import { Module } from "../model/module.ts";
import { fetchAllModules } from "../api/fetchCourse.ts"; import { fetchAllModules } from "../api/fetchCourse.ts";
import moduleStore from "../store/moduleStore.ts"; import moduleStore from "../store/moduleStore.ts";
import { MultiSelectAllChangeEvent } from "primevue/multiselect"; import { MultiSelectAllChangeEvent } from "primevue/multiselect";
import { useDialog } from "primevue/usedialog";
const dialog = useDialog();
import router from "../router"; import router from "../router";
import { fetchModule } from "../api/fetchModule.ts";
const fetchedModules = async () => { const fetchedModules = async () => {
return await fetchAllModules(); return await fetchAllModules();
@@ -30,7 +34,33 @@ async function nextStep() {
await router.push("/rename-modules"); await router.push("/rename-modules");
} }
const display = (module: Module) => module.Name + " (" + module.Course + ")"; const ModuleInformation = defineAsyncComponent(
() => import("./ModuleInformation.vue"),
);
async function showInfo(moduleName: string) {
const module: Ref<Module> = ref(new Module("", "", "", "", "", []));
await fetchModule(moduleName).then((data) => {
module.value = 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 selectAll = ref(false);
@@ -69,11 +99,24 @@ function selectChange() {
@selectall-change="onSelectAllChange($event)" @selectall-change="onSelectAllChange($event)"
> >
<template #option="slotProps"> <template #option="slotProps">
<div class="flex align-items-center"> <div class="flex justify-content-between w-full">
<p class="text-1xl white-space-normal"> <div class="flex align-items-center justify-content-center">
<p class="text-1xl white-space-normal p-mb-0">
{{ display(slotProps.option) }} {{ display(slotProps.option) }}
</p> </p>
</div> </div>
<div class="flex align-items-center justify-content-center ml-2">
<Button
icon="pi pi-info"
severity="secondary"
rounded
outlined
aria-label="Information"
@click.stop="showInfo(slotProps.option.name)"
></Button>
<DynamicDialog />
</div>
</div>
</template> </template>
<template #footer> <template #footer>
<div class="py-2 px-3"> <div class="py-2 px-3">

View File

@@ -42,7 +42,13 @@ async function getModules() {
<template> <template>
<div class="flex flex-column"> <div class="flex flex-column">
<div class="flex align-items-center justify-content-center h-4rem m-2"> <div class="flex align-items-center justify-content-center h-4rem m-2">
<h3 class="text-4xl">Welcome to HTWKalender <i class="pi pi-calendar vertical-align-baseline" style="font-size: 2rem"></i></h3> <h3 class="text-4xl">
Welcome to HTWKalender
<i
class="pi pi-calendar vertical-align-baseline"
style="font-size: 2rem"
></i>
</h3>
</div> </div>
<div <div
class="flex align-items-center justify-content-center h-4rem border-round m-2" class="flex align-items-center justify-content-center h-4rem border-round m-2"

View File

@@ -1,5 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup></script>
</script>
<template> <template>
<div class="flex align-items-center justify-content-center flex-column"> <div class="flex align-items-center justify-content-center flex-column">
@@ -8,81 +7,153 @@
</div> </div>
<div class="flex flex-column col-7"> <div class="flex flex-column col-7">
<div class="grid my-2"> <div class="grid my-2">
<div class="col"> <div class="col">
Wie funktioniert das Kalender erstellen mit dem HTWKalender? Wie funktioniert das Kalender erstellen mit dem HTWKalender?
</div> </div>
<div class="col">Die Webseite ermöglicht es deinen HTWK-Stundenplan in eines deiner bevorzugten Kalender-Verwaltungs-Programme (Outlook, Google Kalender, etc.) einzubinden.</div> <div class="col">
Die Webseite ermöglicht es deinen HTWK-Stundenplan in eines deiner
bevorzugten Kalender-Verwaltungs-Programme (Outlook, Google Kalender,
etc.) einzubinden.
</div>
</div> </div>
<div class="grid my-2"> <div class="grid my-2">
<div class="col">Wie genau funktioniert das alles?</div>
<div class="col"> <div class="col">
Wie genau funktioniert das alles? 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="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>
<div class="grid my-2"> <div class="grid my-2">
<div class="col"> <div class="col">Wie kann ich den Kalender abonnieren?</div>
Wie kann ich den Kalender abonnieren?
</div>
<div class="col"> <div class="col">
<Accordion> <Accordion>
<AccordionTab header="Google Calendar"> <AccordionTab header="Google Calendar">
<ol> <ol>
<li>Erstelle deinen Kalender und kopiere den Link.</li> <li>Erstelle deinen Kalender und kopiere den Link.</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.</li> <li>
<li>Füge den kopierten Kalenderlink ein, klicke auf <em>"Kalender hinzufügen"</em> und du bist fertig.</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.
</li>
<li>
Füge den kopierten Kalenderlink ein, klicke auf
<em>"Kalender hinzufügen"</em> und du bist fertig.
</li>
</ol> </ol>
</AccordionTab> </AccordionTab>
<AccordionTab header="Microsoft Outlook"> <AccordionTab header="Microsoft Outlook">
<p>Unter Outlook 2010:</p> <p>Unter Outlook 2010:</p>
<ol> <ol>
<li>Erstelle deinen Kalender und kopiere den Link.</li> <li>Erstelle deinen Kalender und kopiere den Link.</li>
<li>Klicke auf den Reiter <em>Start</em>.</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>
<li>Es öffnet sich ein Kontextmenü, in dem du den Punkt <em>Aus dem Internet</em> anklickst.</li> Dort befindest sich in der Sektion
<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> <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>
</ol> </ol>
<p>Unter Outlook 2007:</p> <p>Unter Outlook 2007:</p>
<ol> <ol>
<li>Erstelle deinen Kalender und kopiere den Link.</li> <li>Erstelle deinen Kalender und kopiere den Link.</li>
<li>Unter <em>Extras</em> findest du die <em>Kontoeinstellungen</em>.</li> <li>
<li>Dort auf den Reiter <em>Internetkalender</em> klicken.</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>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>
<li>Spätestens beim nächsten Start sollte der Kalender aktualisiert werden und dir fortan zur Verfügung stehen.</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>
</ol> </ol>
</AccordionTab> </AccordionTab>
<AccordionTab header="Kalender (OS X)"> <AccordionTab header="Kalender (OS X)">
<ol> <ol>
<li>Erstelle deinen Kalender und kopiere den Link.</li> <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>
<li>Im daraufhin erscheinenden Fenster musst du den kopierten Link einfügen und <em>abonnieren</em> klicken.</li> Unter <em>Ablage</em> findest du den Punkt
<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> <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>
</ol> </ol>
</AccordionTab> </AccordionTab>
<AccordionTab header="Thunderbird"> <AccordionTab header="Thunderbird">
<ol> <ol>
<li>Erstelle deinen Kalender und kopiere den Link.</li> <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>
<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> Im Menü <em>Termine und Aufgaben</em> den Punkt
<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> <em>Kalender</em> wählen.
<li>Im folgenden Fenster lässt du das <em>Format</em> wie es ist (<em>iCalender</em>).</li> </li>
<li>Unter <em>"Adresse"</em> den kopierten Kalenderlink einfügen.</li> <li>
<li>Anschließend kannst du noch einen Namen vergeben und weitere Einstellungen nach Belieben vornehmen.</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>
</ol> </ol>
</AccordionTab> </AccordionTab>
<AccordionTab header="IPhone"> <AccordionTab header="IPhone">
<p>
<p>Der einfachste Weg unter iOS ist der iCloud-Sync (siehe Anleitung für OS X Kalender). Hier ist der andere Weg:</p> Der einfachste Weg unter iOS ist der iCloud-Sync (siehe
Anleitung für OS X Kalender). Hier ist der andere Weg:
</p>
<ol> <ol>
<li>Erstelle deinen Kalender und kopiere den Link.</li> <li>Erstelle deinen Kalender und kopiere den Link.</li>
@@ -90,23 +161,36 @@
<li>Dort wählst du <em>Mail, Kontakte, Kalender</em>.</li> <li>Dort wählst du <em>Mail, Kontakte, Kalender</em>.</li>
<li><em>Account hinzufügen</em> auswählen.</li> <li><em>Account hinzufügen</em> auswählen.</li>
<li>Ganz unten tippst du auf <em>Andere</em>.</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>
<li>Im daraufhin erscheinenden Textfeld fügst du den Kalenderlink ein und drückst oben auf <em>weiter</em>.</li> Der letzte Punkt ist <em>Kalenderabo hinzufügen</em>. Dort
<li>Du kannst noch eine <em>Beschreibung</em> vergeben. Den Rest solltest du lassen, wie er ist.</li> drauftippen.
<li>Nach kurzer Zeit taucht der abonnierte Kalender in der Kalender-App auf.</li> </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>
</ol> </ol>
</AccordionTab> </AccordionTab>
<AccordionTab header="Android"> <AccordionTab header="Android">
<p> <p>
Unter Android ist die Synchronisierung mit dem Google Kalender die einfachste Variante. Unter Android ist die Synchronisierung mit dem Google Kalender
Schaue bitte in die Google Kalender Anleitung um zu erfahren, wie du den Kalender dort abonnierst. die einfachste Variante. Schaue bitte in die Google Kalender
Anleitung um zu erfahren, wie du den Kalender dort abonnierst.
</p> </p>
</AccordionTab> </AccordionTab>
<AccordionTab header="Windows Phone"> <AccordionTab header="Windows Phone">
<p>
Am einfachsten ist unter Windows Phone die Synchronisierung über
<p>Am einfachsten ist unter Windows Phone die Synchronisierung über Outlook.com:</p> Outlook.com:
</p>
<ol> <ol>
<li>Erstelle deinen Kalender und kopiere den Link.</li> <li>Erstelle deinen Kalender und kopiere den Link.</li>
@@ -116,7 +200,11 @@
<li>Bei <em>Calendar URL</em> den Kalenderlink einfügen.</li> <li>Bei <em>Calendar URL</em> den Kalenderlink einfügen.</li>
<li>Sonstige Einstellungen nach Belieben vornehmen.</li> <li>Sonstige Einstellungen nach Belieben vornehmen.</li>
<li>Auf <em>Subscribe to calendar</em> klicken.</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>
Das Windows-Phone-Gerät muss mit dem gleichen
Outlook.com-Benutzerkonto angemeldet sein. Fortan sollte die
Synchronisierung des Kalenders automatisch erfolgen.
</li>
</ol> </ol>
</AccordionTab> </AccordionTab>
</Accordion> </Accordion>
@@ -126,50 +214,66 @@
<div class="col"> <div class="col">
Kalender abonnieren? Ich will den <em>downloaden</em>! Kalender abonnieren? Ich will den <em>downloaden</em>!
</div> </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, <div class="col">
wenn du den generierten Link einfach in deinem Browser aufrufst. Das kannst du gern tun. Nachdem dein persönlicher Stundenplan erstellt
<br> Bedenke hierbei, dass heruntergeladene Kalender bzw. Stundenpläne sich nicht aktualisieren werden. Das ist nur möglich, wenn du den Kalender abonnierst.</div> 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> </div>
<div class="grid my-2"> <div class="grid my-2">
<div class="col"> <div class="col">
Ich belege zusätzlich Module aus anderen Studiengängen und möchte diese auch in meinem Stundenplan haben. 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>
<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> </div>
<div class="grid my-2"> <div class="grid my-2">
<div class="col my-2"> <div class="col my-2">Aktualisierung Probleme mit dem Kalender?</div>
Aktualisierung Probleme mit dem Kalender? <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>
<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> </div>
<div class="grid my-2"> <div class="grid my-2">
<div class="col"> <div class="col">
Wie lange ist mein Stundenplan bzw. der Link dorthin gültig? Wie lange ist mein Stundenplan bzw. der Link dorthin gültig?
</div> </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">
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> </div>
<div class="grid my-2"> <div class="grid my-2">
<div class="col">Preis und Entwicklung?</div>
<div class="col"> <div class="col">
Preis und Entwicklung? 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>
<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> </div>
<p> <p>
Nicht gefunden, wonach du suchst?<br> Nicht gefunden, wonach du suchst?<br />
<a href="/imprint">Kontakt aufnehmen</a> <a href="/imprint">Kontakt aufnehmen</a>
</p> </p>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,46 +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,85 @@
<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

@@ -7,6 +7,11 @@ const items = ref([
icon: "pi pi-fw pi-plus", icon: "pi pi-fw pi-plus",
url: "/", url: "/",
}, },
{
label: "Edit Calendar",
icon: "pi pi-fw pi-pencil",
url: "/edit",
},
{ {
label: "FAQ", label: "FAQ",
icon: "pi pi-fw pi-book", icon: "pi pi-fw pi-book",

View File

@@ -0,0 +1,39 @@
<script lang="ts" setup>
import { inject } from "vue";
import { Module } from "../model/module.ts";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dialogRef = inject("dialogRef") as any;
const module = dialogRef.value.data.module as Module;
</script>
<template>
<div>
<h2>{{ module.name }}</h2>
<table>
<tr>
<td>Course: {{ module.course }}</td>
</tr>
<tr>
<td>Person: {{ module.prof }}</td>
</tr>
<tr>
<td>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>
</div>
</td>
</tr>
</table>
</div>
</template>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, PropType, Ref, ref, watch } from "vue"; import { computed, ComputedRef, PropType, Ref, ref, watch } from "vue";
import { Module } from "../model/module.ts"; import { Module } from "../model/module.ts";
import moduleStore from "../store/moduleStore"; import moduleStore from "../store/moduleStore";
import router from "../router"; import router from "../router";
@@ -11,24 +11,30 @@ const props = defineProps({
}, },
}); });
type ModuleWithSelection = { module: Module; selected: boolean };
// array of modules with boolean if selected with getter and setter // array of modules with boolean if selected with getter and setter
const modulesWithSelection: Ref<{ module: Module; selected: boolean }[]> = ref( const modulesWithSelection: Ref<ModuleWithSelection[]> = ref(
props.modules.map((module) => { props.modules.map((propModule) => {
return { module: module, selected: false }; return {
module: propModule,
selected: moduleStore().modules.some((module: Module) =>
module.isEqual ? module.isEqual(propModule) : false,
),
};
}), }),
); );
//watch for changes in modules prop and update modulesWithSelection const selectedModules: ComputedRef<Module[]> = computed(() =>
modulesWithSelection.value
function selectedModules(): Module[] {
return modulesWithSelection.value
.filter((module) => module.selected) .filter((module) => module.selected)
.map((module) => module.module); .map((module) => module.module),
} );
const currentModules = computed(() => props.modules); const currentModules = computed(() => props.modules);
function selectAllModules(selection: boolean) { function selectAllModules(selection: boolean) {
console.debug(props.modules);
modulesWithSelection.value.forEach((module) => { modulesWithSelection.value.forEach((module) => {
module.selected = selection; module.selected = selection;
}); });
@@ -36,10 +42,6 @@ function selectAllModules(selection: boolean) {
const allSelected: Ref<boolean> = ref(true); const allSelected: Ref<boolean> = ref(true);
computed(() => {
return modulesWithSelection.value.every((module) => module.selected);
});
watch(currentModules, (newValue: Module[]) => { watch(currentModules, (newValue: Module[]) => {
modulesWithSelection.value = newValue.map((module) => { modulesWithSelection.value = newValue.map((module) => {
return { module: module, selected: false }; return { module: module, selected: false };
@@ -47,33 +49,30 @@ watch(currentModules, (newValue: Module[]) => {
}); });
function nextStep() { function nextStep() {
console.log("next step"); selectedModules.value.forEach((module: Module) => {
selectedModules().forEach((module) => {
moduleStore().addModule(module); moduleStore().addModule(module);
}); });
console.debug(moduleStore().modules);
router.push("/additional-modules"); router.push("/additional-modules");
} }
</script> </script>
<template> <template>
<div class="flex flex-column card-container"> <div class="flex flex-column card-container mx-8 mt-2">
<div class="flex align-items-center justify-content-center mb-3"> <div class="flex align-items-center justify-content-center mb-3">
<Button <Button
:disabled="selectedModules().length < 1" :disabled="selectedModules.length < 1"
class="col-4 justify-content-center" class="col-4 justify-content-center"
@click="nextStep()" @click="nextStep()"
>Next Step >Next Step
</Button> </Button>
</div> </div>
<div class="flex align-items-center justify-content-center"> <div class="flex align-items-center justify-content-center">
<DataView :value="modulesWithSelection" data-key="name"> <DataView :value="modulesWithSelection" data-key="module">
<template #header> <template #header>
<div class="flex justify-content-between flex-wrap"> <div class="flex justify-content-between flex-wrap">
<div class="flex align-items-center justify-content-center"> <div class="flex align-items-center justify-content-center">
<h3>Modules - {{ selectedModules().length }}</h3> <h3>Modules - {{ selectedModules.length }}</h3>
</div> </div>
<div class="flex align-items-center justify-content-center"> <div class="flex align-items-center justify-content-center">
<ToggleButton <ToggleButton
@@ -96,20 +95,18 @@ function nextStep() {
<template #list="slotProps"> <template #list="slotProps">
<div class="col-12"> <div class="col-12">
<div <div
class="flex flex-column xl:flex-row xl:align-items-start p-4 gap-4" class="flex flex-column xl:flex-row xl:align-items-start p-2 gap-4"
> >
<div <div
class="flex flex-column sm:flex-row justify-content-between align-items-center xl:align-items-start flex-1 gap-4" class="flex flex-column sm:flex-row justify-content-between align-items-center flex-1 gap-4"
> >
<div <div
class="flex flex-column align-items-center sm:align-items-start gap-3" class="flex flex-column align-items-center justify-content-center sm:align-items-start gap-3"
> >
<div class="text-2xl"> <p class="text-lg">{{ slotProps.data.module.name }}</p>
{{ slotProps.data.module.Name }}
</div>
</div> </div>
<div <div
class="flex sm:flex-column align-items-center sm:align-items-end gap-3 sm:gap-2" class="flex sm:flex-column justify-content-center sm:align-items-end gap-3 sm:gap-2"
> >
<ToggleButton <ToggleButton
v-model="modulesWithSelection[slotProps.index].selected" v-model="modulesWithSelection[slotProps.index].selected"

View File

@@ -1,5 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup></script>
</script>
<template> <template>
<div class="flex align-items-center justify-content-center flex-column"> <div class="flex align-items-center justify-content-center flex-column">
@@ -9,8 +8,13 @@
<div class="flex flex-column col-7"> <div class="flex flex-column col-7">
<h1>Datenschutzerklärung</h1> <h1>Datenschutzerklärung</h1>
<p>Stand: 19. September 2023</p> <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. <p>
Die verwendeten Begriffe sind nicht geschlechtsspezifisch.</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> <h2>Inhaltsübersicht</h2>
<ul class="index"> <ul class="index">
@@ -173,18 +177,18 @@
Im Rahmen unserer Verarbeitung von personenbezogenen Daten kommt es vor, Im Rahmen unserer Verarbeitung von personenbezogenen Daten kommt es vor,
dass die Daten an andere Stellen, Unternehmen, rechtlich selbstständige dass die Daten an andere Stellen, Unternehmen, rechtlich selbstständige
Organisationseinheiten oder Personen übermittelt oder sie ihnen Organisationseinheiten oder Personen übermittelt oder sie ihnen
gegenüber offengelegt werden. Zu den Empfängern dieser Daten können gegenüber offengelegt werden. Zu den Empfängern dieser Daten können z.B.
z. B. mit IT-Aufgaben beauftragte Dienstleister oder Anbieter von mit IT-Aufgaben beauftragte Dienstleister oder Anbieter von Diensten und
Diensten und Inhalten, die in eine Webseite eingebunden werden, gehören. Inhalten, die in eine Webseite eingebunden werden, gehören. In solchen
In solchen Fällen beachten wir die gesetzlichen Vorgaben und schließen Fällen beachten wir die gesetzlichen Vorgaben und schließen insbesondere
insbesondere entsprechende Verträge bzw. Vereinbarungen, die dem Schutz entsprechende Verträge bzw. Vereinbarungen, die dem Schutz Ihrer Daten
Ihrer Daten dienen, mit den Empfängern Ihrer Daten ab. dienen, mit den Empfängern Ihrer Daten ab.
</p> </p>
<h2 id="m24">Internationale Datentransfers</h2> <h2 id="m24">Internationale Datentransfers</h2>
<p> <p>
Datenverarbeitung in Drittländern: Sofern wir Daten in einem Drittland Datenverarbeitung in Drittländern: Sofern wir Daten in einem Drittland
(d. h., außerhalb der Europäischen Union (EU), des Europäischen (d.h., außerhalb der Europäischen Union (EU), des Europäischen
Wirtschaftsraums (EWR)) verarbeiten oder die Verarbeitung im Rahmen der Wirtschaftsraums (EWR)) verarbeiten oder die Verarbeitung im Rahmen der
Inanspruchnahme von Diensten Dritter oder der Offenlegung bzw. Inanspruchnahme von Diensten Dritter oder der Offenlegung bzw.
Übermittlung von Daten an andere Personen, Stellen oder Unternehmen Übermittlung von Daten an andere Personen, Stellen oder Unternehmen
@@ -293,10 +297,10 @@
<p> <p>
Cookies sind kleine Textdateien, bzw. sonstige Speichervermerke, die Cookies sind kleine Textdateien, bzw. sonstige Speichervermerke, die
Informationen auf Endgeräten speichern und Informationen aus den Informationen auf Endgeräten speichern und Informationen aus den
Endgeräten auslesen. Z. B. um den Login-Status in einem Nutzerkonto, Endgeräten auslesen. Z.B. um den Login-Status in einem Nutzerkonto,
einen Warenkorbinhalt in einem E-Shop, die aufgerufenen Inhalte oder einen Warenkorbinhalt in einem E-Shop, die aufgerufenen Inhalte oder
verwendete Funktionen eines Onlineangebotes speichern. Cookies können verwendete Funktionen eines Onlineangebotes speichern. Cookies können
ferner zu unterschiedlichen Zwecken eingesetzt werden, z. B. zu Zwecken ferner zu unterschiedlichen Zwecken eingesetzt werden, z.B. zu Zwecken
der Funktionsfähigkeit, Sicherheit und Komfort von Onlineangeboten sowie der Funktionsfähigkeit, Sicherheit und Komfort von Onlineangeboten sowie
der Erstellung von Analysen der Besucherströme. der Erstellung von Analysen der Besucherströme.
</p> </p>
@@ -325,7 +329,7 @@
hängt davon ab, ob wir Nutzer um eine Einwilligung bitten. Falls die hängt davon ab, ob wir Nutzer um eine Einwilligung bitten. Falls die
Nutzer einwilligen, ist die Rechtsgrundlage der Verarbeitung Ihrer Daten Nutzer einwilligen, ist die Rechtsgrundlage der Verarbeitung Ihrer Daten
die erklärte Einwilligung. Andernfalls werden die mithilfe von Cookies die erklärte Einwilligung. Andernfalls werden die mithilfe von Cookies
verarbeiteten Daten auf Grundlage unserer berechtigten Interessen (z. B. verarbeiteten Daten auf Grundlage unserer berechtigten Interessen (z.B.
an einem betriebswirtschaftlichen Betrieb unseres Onlineangebotes und an einem betriebswirtschaftlichen Betrieb unseres Onlineangebotes und
Verbesserung seiner Nutzbarkeit) verarbeitet oder, wenn dies im Rahmen Verbesserung seiner Nutzbarkeit) verarbeitet oder, wenn dies im Rahmen
der Erfüllung unserer vertraglichen Pflichten erfolgt, wenn der Einsatz der Erfüllung unserer vertraglichen Pflichten erfolgt, wenn der Einsatz
@@ -335,15 +339,16 @@
von unseren Einwilligungs- und Verarbeitungsprozessen auf. von unseren Einwilligungs- und Verarbeitungsprozessen auf.
</p> </p>
<p> <p>
<strong>Speicherdauer: </strong>Im Hinblick auf die Speicherdauer werden <strong>Speicherdauer:</strong>Im Hinblick auf die Speicherdauer werden
die folgenden Arten von Cookies unterschieden: die folgenden Arten von Cookies unterschieden:
</p> </p>
<ul> <ul>
<li> <li>
<strong <strong
>Temporäre Cookies (auch: Session- oder Sitzungs-Cookies):</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 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. Applikation) geschlossen hat.
</li> </li>
<li> <li>
@@ -352,9 +357,9 @@
beispielsweise der Login-Status gespeichert oder bevorzugte Inhalte beispielsweise der Login-Status gespeichert oder bevorzugte Inhalte
direkt angezeigt werden, wenn der Nutzer eine Website erneut besucht. direkt angezeigt werden, wenn der Nutzer eine Website erneut besucht.
Ebenso können die mit Hilfe von Cookies erhobenen Daten der Nutzer zur Ebenso können die mit Hilfe von Cookies erhobenen Daten der Nutzer zur
Reichweitenmessung verwendet werden. Sofern wir Nutzern keine Reichweitenmessung verwendet werden. Sofern wir Nutzern keine
expliziten Angaben zur Art und Speicherdauer von Cookies mitteilen expliziten Angaben zur Art und Speicherdauer von Cookies mitteilen
(z. B. im Rahmen der Einholung der Einwilligung), sollten Nutzer davon (z.B. im Rahmen der Einholung der Einwilligung), sollten Nutzer davon
ausgehen, dass Cookies permanent sind und die Speicherdauer bis zu ausgehen, dass Cookies permanent sind und die Speicherdauer bis zu
zwei Jahre betragen kann. zwei Jahre betragen kann.
</li> </li>
@@ -411,7 +416,7 @@
der Speicherung der Einwilligung kann bis zu zwei Jahren betragen. der Speicherung der Einwilligung kann bis zu zwei Jahren betragen.
Hierbei wird ein pseudonymer Nutzer-Identifikator gebildet und mit dem Hierbei wird ein pseudonymer Nutzer-Identifikator gebildet und mit dem
Zeitpunkt der Einwilligung, Angaben zur Reichweite der Einwilligung Zeitpunkt der Einwilligung, Angaben zur Reichweite der Einwilligung
(z. B. welche Kategorien von Cookies und/oder Diensteanbieter) sowie (z.B. welche Kategorien von Cookies und/oder Diensteanbieter) sowie
dem Browser, System und verwendeten Endgerät gespeichert; dem Browser, System und verwendeten Endgerät gespeichert;
<span class="" <span class=""
><strong>Rechtsgrundlagen:</strong> Einwilligung (Art. 6 Abs. 1 S. 1 ><strong>Rechtsgrundlagen:</strong> Einwilligung (Art. 6 Abs. 1 S. 1
@@ -429,14 +434,14 @@
</p> </p>
<ul class="m-elements"> <ul class="m-elements">
<li> <li>
<strong>Verarbeitete Datenarten:</strong> Nutzungsdaten (z. B. <strong>Verarbeitete Datenarten:</strong> Nutzungsdaten (z.B. besuchte
besuchte Webseiten, Interesse an Inhalten, Zugriffszeiten); Meta-, Webseiten, Interesse an Inhalten, Zugriffszeiten); Meta-,
Kommunikations- und Verfahrensdaten (z. .B. IP-Adressen, Zeitangaben, Kommunikations- und Verfahrensdaten (z.B. IP-Adressen, Zeitangaben,
Identifikationsnummern, Einwilligungsstatus). Identifikationsnummern, Einwilligungsstatus).
</li> </li>
<li> <li>
<strong>Betroffene Personen:</strong> Nutzer (z. .B. <strong>Betroffene Personen:</strong> Nutzer (z..B. Webseitenbesucher,
Webseitenbesucher, Nutzer von Onlinediensten). Nutzer von Onlinediensten).
</li> </li>
<li> <li>
<strong>Zwecke der Verarbeitung:</strong> Bereitstellung unseres <strong>Zwecke der Verarbeitung:</strong> Bereitstellung unseres
@@ -477,8 +482,8 @@
erfolgreichen Abruf, Browsertyp nebst Version, das Betriebssystem des erfolgreichen Abruf, Browsertyp nebst Version, das Betriebssystem des
Nutzers, Referrer URL (die zuvor besuchte Seite) und im Regelfall Nutzers, Referrer URL (die zuvor besuchte Seite) und im Regelfall
IP-Adressen und der anfragende Provider gehören. Die Serverlogfiles IP-Adressen und der anfragende Provider gehören. Die Serverlogfiles
können zum einen zu Zwecken der Sicherheit eingesetzt werden, z. B., können zum einen zu Zwecken der Sicherheit eingesetzt werden, z.B., um
um eine Überlastung der Server zu vermeiden (insbesondere im Fall von eine Überlastung der Server zu vermeiden (insbesondere im Fall von
missbräuchlichen Angriffen, sogenannten DDoS-Attacken) und zum missbräuchlichen Angriffen, sogenannten DDoS-Attacken) und zum
anderen, um die Auslastung der Server und ihre Stabilität anderen, um die Auslastung der Server und ihre Stabilität
sicherzustellen; sicherzustellen;
@@ -494,18 +499,18 @@
</ul> </ul>
<h2 id="m182">Kontakt- und Anfragenverwaltung</h2> <h2 id="m182">Kontakt- und Anfragenverwaltung</h2>
<p> <p>
Bei der Kontaktaufnahme mit uns (z. B. per Post, Kontaktformular, Bei der Kontaktaufnahme mit uns (z.B. per Post, Kontaktformular, E-Mail,
E-Mail, Telefon oder via soziale Medien) sowie im Rahmen bestehender Telefon oder via soziale Medien) sowie im Rahmen bestehender Nutzer- und
Nutzer- und Geschäftsbeziehungen werden die Angaben der anfragenden Geschäftsbeziehungen werden die Angaben der anfragenden Personen
Personen verarbeitet soweit dies zur Beantwortung der Kontaktanfragen verarbeitet soweit dies zur Beantwortung der Kontaktanfragen und
und etwaiger angefragter Maßnahmen erforderlich ist. etwaiger angefragter Maßnahmen erforderlich ist.
</p> </p>
<ul class="m-elements"> <ul class="m-elements">
<li> <li>
<strong>Verarbeitete Datenarten:</strong> Kontaktdaten (z. B. E-Mail, <strong>Verarbeitete Datenarten:</strong> Kontaktdaten (z.B. E-Mail,
Telefonnummern); Inhaltsdaten (z. B. Eingaben in Onlineformularen); Telefonnummern); Inhaltsdaten (z.B. Eingaben in Onlineformularen);
Nutzungsdaten (z. B. besuchte Webseiten, Interesse an Inhalten, Nutzungsdaten (z.B. besuchte Webseiten, Interesse an Inhalten,
Zugriffszeiten); Meta-, Kommunikations- und Verfahrensdaten (z. .B. Zugriffszeiten); Meta-, Kommunikations- und Verfahrensdaten (z.B.
IP-Adressen, Zeitangaben, Identifikationsnummern, IP-Adressen, Zeitangaben, Identifikationsnummern,
Einwilligungsstatus). Einwilligungsstatus).
</li> </li>
@@ -522,91 +527,107 @@
</li> </li>
</ul> </ul>
<h2 id="m10">Rechte der betroffenen Personen</h2> <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 <p>
bis 21 DSGVO Ihnen stehen als Betroffene nach der DSGVO verschiedene Rechte zu, die
ergeben:</p> sich insbesondere aus Art. 15 bis 21 DSGVO ergeben:
</p>
<ul> <ul>
<li><strong>Widerspruchsrecht: Sie haben das Recht, aus Gründen, die sich aus Ihrer besonderen <li>
Situation <strong
ergeben, jederzeit gegen die Verarbeitung der Sie betreffenden personenbezogenen Daten, die >Widerspruchsrecht: Sie haben das Recht, aus Gründen, die sich aus
aufgrund von Ihrer besonderen Situation ergeben, jederzeit gegen die Verarbeitung
Art. 6 Abs. 1 lit. e oder f DSGVO erfolgt, Widerspruch einzulegen; dies gilt auch für ein der Sie betreffenden personenbezogenen Daten, die aufgrund von Art.
auf diese 6 Abs. 1 lit. e oder f DSGVO erfolgt, Widerspruch einzulegen; dies
Bestimmungen gestütztes Profiling. Werden die Sie betreffenden personenbezogenen Daten gilt auch für ein auf diese Bestimmungen gestütztes Profiling.
verarbeitet, um Werden die Sie betreffenden personenbezogenen Daten verarbeitet, um
Direktwerbung zu betreiben, haben Sie das Recht, jederzeit Widerspruch gegen die Direktwerbung zu betreiben, haben Sie das Recht, jederzeit
Verarbeitung der Sie Widerspruch gegen die Verarbeitung der Sie betreffenden
betreffenden personenbezogenen Daten zum Zwecke derartiger Werbung einzulegen; dies gilt personenbezogenen Daten zum Zwecke derartiger Werbung einzulegen;
auch für das dies gilt auch für das Profiling, soweit es mit solcher
Profiling, soweit es mit solcher Direktwerbung in Verbindung steht.</strong></li> Direktwerbung in Verbindung steht.</strong
<li><strong>Widerrufsrecht bei Einwilligungen:</strong> Sie haben das Recht, erteilte Einwilligungen >
jederzeit </li>
zu widerrufen.</li> <li>
<li><strong>Auskunftsrecht:</strong> Sie haben das Recht, eine Bestätigung darüber zu verlangen, ob <strong>Widerrufsrecht bei Einwilligungen:</strong> Sie haben das
betreffende Recht, erteilte Einwilligungen jederzeit zu widerrufen.
Daten verarbeitet werden und auf Auskunft über diese Daten sowie auf weitere Informationen und </li>
Kopie der <li>
Daten entsprechend den gesetzlichen Vorgaben.</li> <strong>Auskunftsrecht:</strong> Sie haben das Recht, eine Bestätigung
<li><strong>Recht auf Berichtigung:</strong> Sie haben entsprechend den gesetzlichen Vorgaben das darüber zu verlangen, ob betreffende Daten verarbeitet werden und auf
Recht, die Auskunft über diese Daten sowie auf weitere Informationen und Kopie
Vervollständigung der Sie betreffenden Daten oder die Berichtigung der Sie betreffenden der Daten entsprechend den gesetzlichen Vorgaben.
unrichtigen Daten zu </li>
verlangen.</li> <li>
<li><strong>Recht auf Löschung und Einschränkung der Verarbeitung:</strong> Sie haben nach Maßgabe <strong>Recht auf Berichtigung:</strong> Sie haben entsprechend den
der gesetzlichen Vorgaben das Recht, die Vervollständigung der Sie
gesetzlichen Vorgaben das Recht, zu verlangen, dass Sie betreffende Daten unverzüglich gelöscht betreffenden Daten oder die Berichtigung der Sie betreffenden
werden, bzw. unrichtigen Daten zu verlangen.
alternativ nach Maßgabe der gesetzlichen Vorgaben eine Einschränkung der Verarbeitung der Daten </li>
zu <li>
verlangen.</li> <strong
<li><strong>Recht auf Datenübertragbarkeit:</strong> Sie haben das Recht, Sie betreffende Daten, die >Recht auf Löschung und Einschränkung der Verarbeitung:</strong
Sie uns >
bereitgestellt haben, nach Maßgabe der gesetzlichen Vorgaben in einem strukturierten, gängigen Sie haben nach Maßgabe der gesetzlichen Vorgaben das Recht, zu
und verlangen, dass Sie betreffende Daten unverzüglich gelöscht werden,
maschinenlesbaren Format zu erhalten oder deren Übermittlung an einen anderen Verantwortlichen bzw. alternativ nach Maßgabe der gesetzlichen Vorgaben eine
zu fordern. 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> </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> </ul>
<h2 id="m42">Begriffsdefinitionen</h2> <h2 id="m42">Begriffsdefinitionen</h2>
<p>In diesem Abschnitt erhalten Sie eine Übersicht über die in dieser Datenschutzerklärung verwendeten <p>
Begrifflichkeiten. Viele der Begriffe sind dem Gesetz entnommen und vor allem im Art. 4 DSGVO In diesem Abschnitt erhalten Sie eine Übersicht über die in dieser
definiert. Die Datenschutzerklärung verwendeten Begrifflichkeiten. Viele der Begriffe
gesetzlichen Definitionen sind verbindlich. Die nachfolgenden Erläuterungen sollen dagegen vor allem sind dem Gesetz entnommen und vor allem im Art. 4 DSGVO definiert. Die
dem gesetzlichen Definitionen sind verbindlich. Die nachfolgenden
Verständnis dienen. Die Begriffe sind alphabetisch sortiert.</p> Erläuterungen sollen dagegen vor allem dem Verständnis dienen. Die
Begriffe sind alphabetisch sortiert.
</p>
<ul class="glossary"> <ul class="glossary">
<li><strong>Personenbezogene Daten:</strong> "Personenbezogene Daten“ sind alle Informationen, die <li>
sich auf eine <strong>Personenbezogene Daten:</strong> "Personenbezogene Daten“ sind
identifizierte oder identifizierbare natürliche Person (im Folgenden "betroffene Person) alle Informationen, die sich auf eine identifizierte oder
beziehen; als identifizierbare natürliche Person (im Folgenden "betroffene Person)
identifizierbar wird eine natürliche Person angesehen, die direkt oder indirekt, insbesondere beziehen; als identifizierbar wird eine natürliche Person angesehen,
mittels die direkt oder indirekt, insbesondere mittels Zuordnung zu einer
Zuordnung zu einer Kennung wie einem Namen, zu einer Kennnummer, zu Standortdaten, zu einer Kennung wie einem Namen, zu einer Kennnummer, zu Standortdaten, zu
Online-Kennung einer Online-Kennung (z.B. Cookie) oder zu einem oder mehreren
(z.B. Cookie) oder zu einem oder mehreren besonderen Merkmalen identifiziert werden kann, die besonderen Merkmalen identifiziert werden kann, die Ausdruck der
Ausdruck der physischen, physiologischen, genetischen, psychischen,
physischen, physiologischen, genetischen, psychischen, wirtschaftlichen, kulturellen oder wirtschaftlichen, kulturellen oder sozialen Identität dieser
sozialen Identität natürlichen Person sind.
dieser natürlichen Person sind. </li> </li>
<li><strong>Verantwortlicher:</strong> Als "Verantwortlicher“ wird die natürliche oder juristische <li>
Person, <strong>Verantwortlicher:</strong> Als "Verantwortlicher“ wird die
Behörde, Einrichtung oder andere Stelle, die allein oder gemeinsam mit anderen über die Zwecke natürliche oder juristische Person, Behörde, Einrichtung oder andere
und Mittel Stelle, die allein oder gemeinsam mit anderen über die Zwecke und
der Verarbeitung von personenbezogenen Daten entscheidet, bezeichnet. </li> Mittel der Verarbeitung von personenbezogenen Daten entscheidet,
<li><strong>Verarbeitung:</strong> "Verarbeitung" ist jeder mit oder ohne Hilfe automatisierter bezeichnet.
Verfahren </li>
ausgeführte Vorgang oder jede solche Vorgangsreihe im Zusammenhang mit personenbezogenen Daten. <li>
Der Begriff <strong>Verarbeitung:</strong> "Verarbeitung" ist jeder mit oder ohne
reicht weit und umfasst praktisch jeden Umgang mit Daten, sei es das Erheben, das Auswerten, das Hilfe automatisierter Verfahren ausgeführte Vorgang oder jede solche
Speichern, Vorgangsreihe im Zusammenhang mit personenbezogenen Daten. Der Begriff
das Übermitteln oder das Löschen. </li> 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> </ul>
<p class="seal"> <p class="seal">
<a <a

View File

@@ -5,17 +5,18 @@ import router from "../router";
import tokenStore from "../store/tokenStore.ts"; import tokenStore from "../store/tokenStore.ts";
import { ref } from "vue"; import { ref } from "vue";
const tableData = ref(moduleStore().modules.map((module) => { const tableData = ref(
moduleStore().modules.map((module) => {
return { return {
Course: module.Course, Course: module.course,
Module: module, Module: module,
} };
}) }),
); );
const columns = ref([ const columns = ref([
{ field: 'Course', header: 'Course' }, { field: "Course", header: "Course" },
{ field: 'Module', header: 'Module' }, { field: "Module", header: "Module" },
]); ]);
async function finalStep() { async function finalStep() {
@@ -28,22 +29,38 @@ async function finalStep() {
<template> <template>
<div class="flex flex-column"> <div class="flex flex-column">
<div class="flex align-items-center justify-content-center h-4rem m-2"> <div class="flex align-items-center justify-content-center h-4rem m-2">
<h3> <h3>Rename your selected Modules to your liking.</h3>
Rename your selected Modules to your liking.
</h3>
</div> </div>
<div class="card flex align-items-center justify-content-center m-2"> <div class="card flex align-items-center justify-content-center m-2">
<DataTable :value="tableData" editMode="cell" tableClass="editable-cells-table" responsiveLayout="scroll"> <DataTable
<Column v-for="col of columns" :key="col.field" :field="col.field" :header="col.header"> :value="tableData"
<template #body="{ data, field }" > edit-mode="cell"
<div>{{ field === 'Module' ? data[field].UserDefinedName : data[field] }}</div> table-class="editable-cells-table"
responsive-layout="scroll"
>
<Column
v-for="col of columns"
:key="col.field"
:field="col.field"
:header="col.header"
>
<template #body="{ data, field }">
<div>
{{
field === "Module" ? data[field].userDefinedName : data[field]
}}
</div>
</template> </template>
<template #editor="{ data, field }"> <template #editor="{ data, field }">
<template v-if="field !== 'Module'"> <template v-if="field !== 'Module'">
<div>{{ data[field] }}</div> <div>{{ data[field] }}</div>
</template> </template>
<template v-else> <template v-else>
<InputText class="w-full" v-model="data[field].UserDefinedName" autofocus /> <InputText
v-model="data[field].userDefinedName"
class="w-full"
autofocus
/>
</template> </template>
</template> </template>
</Column> </Column>
@@ -54,10 +71,6 @@ async function finalStep() {
<Button @click="finalStep()">Next Step</Button> <Button @click="finalStep()">Next Step</Button>
</div> </div>
</div> </div>
</template> </template>
<style scoped> <style scoped></style>
</style>

View File

@@ -0,0 +1,144 @@
<script lang="ts" setup>
import { defineAsyncComponent, ref, Ref } from "vue";
import { Module } from "../../model/module";
import { fetchAllModules } from "../../api/fetchCourse";
import moduleStore from "../../store/moduleStore";
import { MultiSelectAllChangeEvent } from "primevue/multiselect";
import router from "../../router";
import { fetchModule } from "../../api/fetchModule";
import { useDialog } from "primevue/usedialog";
const dialog = useDialog();
const fetchedModules = async () => {
return await fetchAllModules();
};
const modules: Ref<Module[]> = ref([]);
const selectedModules: Ref<Module[]> = ref([] as Module[]);
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("/edit-calendar");
}
const ModuleInformation = defineAsyncComponent(
() => import("../ModuleInformation.vue"),
);
async function showInfo(moduleName: string) {
const module: Ref<Module> = ref(new Module("", "", "", "", "", []));
await fetchModule(moduleName).then((data) => {
module.value = 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) => {
selectedModules.value = event.checked
? modules.value.map((module) => module)
: [];
selectAll.value = event.checked;
};
function selectChange() {
selectAll.value = selectedModules.value.length === 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">
<MultiSelect
v-model="selectedModules"
: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()"
@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"
@click.stop="showInfo(slotProps.option.name)"
></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

@@ -0,0 +1,107 @@
<script lang="ts" setup>
import { computed, Ref, ref } from "vue";
import { Module } from "../../model/module";
import moduleStore from "../../store/moduleStore";
import { fetchAllModules } from "../../api/fetchCourse";
import { saveIndividualFeed } from "../../api/createFeed";
import tokenStore from "../../store/tokenStore";
import router from "../../router";
const tableData = computed(() =>
moduleStore().modules.map((module: Module) => {
return {
Course: module.course,
Module: module,
};
}),
);
const columns = ref([
{ field: "Course", header: "Course" },
{ field: "Module", header: "Module" },
]);
const fetchedModules = async () => {
return await fetchAllModules();
};
function deleteModule(module: Module) {
console.debug(module);
moduleStore().removeModule(module);
}
const modules: Ref<Module[]> = ref([]);
fetchedModules().then(
(data) =>
(modules.value = data.map((module: Module) => {
return module;
})),
);
async function finalStep() {
await saveIndividualFeed(tokenStore().token, moduleStore().modules);
await router.push("/calendar-link");
}
</script>
<template>
<div class="flex flex-column card-container mx-8 mt-2">
<div
class="flex align-items-center justify-content-center border-round m-2"
>
<DataTable
:value="tableData"
edit-mode="cell"
table-class="editable-cells-table"
responsive-layout="scroll"
>
<Column
v-for="col of columns"
:key="col.field"
:field="col.field"
:header="col.header"
>
<template #body="{ data, field }">
<div>
{{
field === "Module" ? data[field].userDefinedName : data[field]
}}
</div>
</template>
<template #editor="{ data, field }">
<template v-if="field !== 'Module'">
<div>{{ data[field] }}</div>
</template>
<template v-else>
<InputText
v-model="data[field].userDefinedName"
class="w-full"
autofocus
/>
</template>
</template>
</Column>
<Column>
<template #body="{ data }">
<Button
icon="pi pi-trash"
severity="danger"
outlined
rounded
aria-label="Cancel"
@click="deleteModule(data['Module'])"
/>
</template>
</Column>
</DataTable>
</div>
<div
class="flex align-items-center justify-content-center border-round m-2"
>
<Button label="Save Calendar" @click="finalStep()" />
</div>
</div>
</template>
<style scoped></style>

View File

@@ -19,10 +19,13 @@ import { createPinia } from "pinia";
import MultiSelect from "primevue/multiselect"; import MultiSelect from "primevue/multiselect";
import ToastService from "primevue/toastservice"; import ToastService from "primevue/toastservice";
import Toast from "primevue/toast"; import Toast from "primevue/toast";
import Accordion from 'primevue/accordion'; import Accordion from "primevue/accordion";
import AccordionTab from 'primevue/accordiontab'; import AccordionTab from "primevue/accordiontab";
import DataTable from "primevue/datatable"; import DataTable from "primevue/datatable";
import Column from "primevue/column"; import Column from "primevue/column";
import DynamicDialog from "primevue/dynamicdialog";
import DialogService from "primevue/dialogservice";
import ProgressSpinner from "primevue/progressspinner";
const app = createApp(App); const app = createApp(App);
const pinia = createPinia(); const pinia = createPinia();
@@ -31,6 +34,7 @@ app.use(PrimeVue);
app.use(router); app.use(router);
app.use(ToastService); app.use(ToastService);
app.use(pinia); app.use(pinia);
app.use(DialogService);
app.component("Button", Button); app.component("Button", Button);
app.component("Menubar", Menubar); app.component("Menubar", Menubar);
app.component("Dropdown", Dropdown); app.component("Dropdown", Dropdown);
@@ -46,4 +50,6 @@ app.component("Accordion", Accordion);
app.component("AccordionTab", AccordionTab); app.component("AccordionTab", AccordionTab);
app.component("DataTable", DataTable); app.component("DataTable", DataTable);
app.component("Column", Column); app.component("Column", Column);
app.component("DynamicDialog", DynamicDialog);
app.component("ProgressSpinner", ProgressSpinner);
app.mount("#app"); app.mount("#app");

View File

@@ -0,0 +1,10 @@
import { Module } from "./module";
export type Calendar = {
collectionId: string;
collectionName: string;
created: string;
id: string;
modules: Module[];
updated: string;
};

View File

@@ -1,16 +1,16 @@
export class Event { export class Event {
constructor( constructor(
public Days: string, public bookedAt: string,
public Week: string, public course: string,
public Start: string, public day: string,
public End: string, public end: string,
public Name: string, public eventType: string,
public EventType: string, public name: string,
public Prof: string, public notes: string,
public Rooms: string, public prof: string,
public Notes: string, public rooms: string,
public BookedAt: string, public semester: string,
public Course: string, public start: string,
public Semester: string, public week: string,
) {} ) {}
} }

View File

@@ -1,7 +1,16 @@
import { Event } from "./event";
export class Module { export class Module {
constructor( constructor(
public Name: string, public name: string,
public Course: string, public course: string,
public UserDefinedName: string, public userDefinedName: string,
public prof: string,
public semester: string,
public events: Event[] = [],
) {} ) {}
isEqual(module: Module): Boolean {
return this.name === module.name && this.course === module.course;
}
} }

View File

@@ -3,10 +3,13 @@ import Faq from "../components/FaqPage.vue";
import CourseSelection from "../components/CourseSelection.vue"; import CourseSelection from "../components/CourseSelection.vue";
import AdditionalModules from "../components/AdditionalModules.vue"; import AdditionalModules from "../components/AdditionalModules.vue";
import CalendarLink from "../components/CalendarLink.vue"; import CalendarLink from "../components/CalendarLink.vue";
import Impress from "../components/Imprint.vue"; import Imprint from "../components/ImprintPage.vue";
import PrivacyPolicy from "../components/PrivacyPolicy.vue"; import PrivacyPolicy from "../components/PrivacyPolicy.vue";
import RenameModules from "../components/RenameModules.vue"; import RenameModules from "../components/RenameModules.vue";
import RoomFinder from "../components/RoomFinder.vue"; import RoomFinder from "../components/RoomFinder.vue";
import EditCalendarView from "../view/editCalendarView.vue";
import EditAdditionalModules from "../components/editCalendar/EditAdditionalModules.vue";
import EditModules from "../components/editCalendar/EditModules.vue";
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@@ -31,20 +34,35 @@ const router = createRouter({
name: "additional-modules", name: "additional-modules",
component: AdditionalModules, component: AdditionalModules,
}, },
{
path: "/edit-additional-modules",
name: "edit-additional-modules",
component: EditAdditionalModules,
},
{
path: "/edit-calendar",
name: "edit-calendar",
component: EditModules,
},
{ {
path: "/calendar-link", path: "/calendar-link",
name: "calendar-link", name: "calendar-link",
component: CalendarLink, component: CalendarLink,
}, },
{
path: "/edit",
name: "edit",
component: EditCalendarView,
},
{ {
path: "/privacy-policy", path: "/privacy-policy",
name: "privacy-policy", name: "privacy-policy",
component: PrivacyPolicy, component: PrivacyPolicy,
}, },
{ {
path: "/impress", path: "/imprint",
name: "impress", name: "imprint",
component: Impress, component: Imprint,
}, },
{ {
path: "/rename-modules", path: "/rename-modules",

View File

@@ -12,6 +12,9 @@ const moduleStore = defineStore("moduleStore", {
removeModule(module: Module) { removeModule(module: Module) {
this.modules.splice(this.modules.indexOf(module), 1); this.modules.splice(this.modules.indexOf(module), 1);
}, },
removeAllModules() {
this.modules = [];
},
}, },
}); });

View File

@@ -0,0 +1,56 @@
<script setup lang="ts">
import { Ref, ref } from "vue";
import { Module } from "../model/module";
import moduleStore from "../store/moduleStore";
import { getCalender } from "../api/loadCalendar";
import router from "../router";
import tokenStore from "../store/tokenStore";
const token: Ref<string> = ref("");
const modules: Ref<Module[]> = ref(moduleStore().modules);
function loadCalendar() {
moduleStore().removeAllModules();
tokenStore().setToken(token.value);
getCalender(token.value).then((data) => {
data.forEach((module) => {
moduleStore().addModule(module);
});
modules.value = data;
});
router.push("/edit-additional-modules");
}
</script>
<template>
<div class="flex flex-column">
<div class="flex align-items-center justify-content-center h-4rem mt-2">
<h3 class="text-2xl">
Edit your HTWKalender
<i
class="pi pi-calendar vertical-align-baseline"
style="font-size: 2rem"
></i>
</h3>
</div>
<div
class="flex align-items-center justify-content-center h-4rem border-round"
>
<p class="text-2xl">Please enter your existing calendar token</p>
</div>
<div
class="flex align-items-center justify-content-center border-round m-2"
>
<InputText v-model="token" type="text" />
</div>
<div
class="flex align-items-center justify-content-center border-round m-2"
>
<Button label="Load Calendar" @click="loadCalendar" />
</div>
</div>
</template>
<style scoped></style>

View File

@@ -9,6 +9,6 @@ export default defineConfig({
port: 8000, port: 8000,
watch: { watch: {
usePolling: true, usePolling: true,
} },
} },
}); });