Archived
Private
Public Access
1
0

Improved substitution page

This commit is contained in:
2023-04-24 18:50:10 +02:00
parent 45a7457ef3
commit 5f3016eccb
12 changed files with 186 additions and 53 deletions

View File

@@ -1,4 +1,5 @@
using BetterIServ.Backend.Entities; using BetterIServ.Backend.Entities;
using HtmlAgilityPack;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using PuppeteerSharp; using PuppeteerSharp;
using Credentials = BetterIServ.Backend.Entities.Credentials; using Credentials = BetterIServ.Backend.Entities.Credentials;
@@ -6,7 +7,7 @@ using Credentials = BetterIServ.Backend.Entities.Credentials;
namespace BetterIServ.Backend.Controllers; namespace BetterIServ.Backend.Controllers;
[ApiController] [ApiController]
[Route("auth")] [Route("iserv")]
public class AuthController : ControllerBase { public class AuthController : ControllerBase {
[HttpPost("login")] [HttpPost("login")]
@@ -47,4 +48,28 @@ public class AuthController : ControllerBase {
return authKeys; return authKeys;
} }
[HttpPost("groups")]
public async Task<ActionResult<SingleResult<string[]>>> GetCourses([FromBody] AuthKeys keys, [FromQuery] string domain) {
var client = new HttpClient();
var request = new HttpRequestMessage {
Method = HttpMethod.Get,
RequestUri = new Uri($"https://{domain}/iserv/profile"),
Headers = {
{ "cookie", keys.ToCookieString() }
}
};
var raw = await (await client.SendAsync(request)).Content.ReadAsStringAsync();
var html = new HtmlDocument();
html.LoadHtml(raw);
var list = html.DocumentNode.SelectSingleNode("//body/div/div[2]/div[3]/div/div/div[2]/div/div/div/div/ul[1]");
var courses = new List<string>();
foreach (var child in list.ChildNodes) {
if (child.ChildNodes.Count < 1) continue;
courses.Add(child.ChildNodes[0].InnerText);
}
return new SingleResult<string[]> { Value = courses.ToArray() };
}
} }

View File

@@ -42,16 +42,32 @@ public class UnitsController : ControllerBase {
if (node.ChildNodes.Count < 9) continue; if (node.ChildNodes.Count < 9) continue;
var substitution = new Substitution { var substitution = new Substitution {
Class = node.ChildNodes[0].InnerText,
Times = node.ChildNodes[1].InnerText.Split(" - ").Select(int.Parse).ToArray(), Times = node.ChildNodes[1].InnerText.Split(" - ").Select(int.Parse).ToArray(),
Type = node.ChildNodes[2].InnerText, Type = node.ChildNodes[2].InnerText,
Representative = node.ChildNodes[3].InnerText, Representative = node.ChildNodes[3].InnerText,
Lesson = node.ChildNodes[4].InnerText, NewLesson = node.ChildNodes[4].InnerText,
Lesson = node.ChildNodes[5].InnerText,
Room = node.ChildNodes[6].InnerText, Room = node.ChildNodes[6].InnerText,
Teacher = node.ChildNodes[7].InnerText, Teacher = node.ChildNodes[7].InnerText,
Description = node.ChildNodes[9].InnerText Description = node.ChildNodes[9].InnerText
}; };
var classes = node.ChildNodes[0].InnerText;
if (!classes.StartsWith("Q")) {
string grade = new string(classes.ToCharArray().Where(char.IsNumber).ToArray());
var subClasses = classes.Replace(grade, "").ToCharArray();
var result = new string[subClasses.Length];
for (int j = 0; j < subClasses.Length; j++) {
result[j] = grade + subClasses[j];
}
substitution.Classes = result;
}
else {
substitution.Classes = new[] { classes };
}
data.Substitutions.Add(substitution); data.Substitutions.Add(substitution);
} }

View File

@@ -6,4 +6,8 @@ public struct AuthKeys {
public string AuthSid { get; set; } public string AuthSid { get; set; }
public string SatId { get; set; } public string SatId { get; set; }
public string AuthSession { get; set; } public string AuthSession { get; set; }
public string ToCookieString() {
return $"IServSession={Session}; IServSAT={Sat}; IServAuthSID={AuthSid}; IServSATId={SatId}; IServAuthSession={AuthSession}";
}
} }

View File

@@ -1,11 +1,12 @@
namespace BetterIServ.Backend.Entities; namespace BetterIServ.Backend.Entities;
public struct Substitution { public struct Substitution {
public string Class { get; set; } public string[] Classes { get; set; }
public int[] Times { get; set; } public int[] Times { get; set; }
public string Type { get; set; } public string Type { get; set; }
public string Representative { get; set; } public string Representative { get; set; }
public string Lesson { get; set; } public string Lesson { get; set; }
public string NewLesson { get; set; }
public string Room { get; set; } public string Room { get; set; }
public string Teacher { get; set; } public string Teacher { get; set; }
public string Description { get; set; } public string Description { get; set; }

View File

@@ -9,6 +9,7 @@ import {firstValueFrom} from "rxjs";
export class IServService { export class IServService {
public userdata?: Userdata; public userdata?: Userdata;
public keys?: AuthKeys;
public backend: string = "http://localhost:5273"; public backend: string = "http://localhost:5273";
constructor(private client: HttpClient) { constructor(private client: HttpClient) {
@@ -16,6 +17,11 @@ export class IServService {
if (data != null) { if (data != null) {
this.userdata = JSON.parse(data); this.userdata = JSON.parse(data);
} }
const keys = localStorage.getItem("keys");
if (keys != null) {
this.keys = JSON.parse(keys);
}
} }
public async login(email: string, password: string): Promise<boolean> { public async login(email: string, password: string): Promise<boolean> {
@@ -27,8 +33,9 @@ export class IServService {
}; };
try { try {
await firstValueFrom(this.client.post(this.backend + "/auth/login", this.userdata)); const keys = await firstValueFrom(this.client.post<AuthKeys>(this.backend + "/iserv/login", this.userdata));
localStorage.setItem("userdata", JSON.stringify(this.userdata)); localStorage.setItem("userdata", JSON.stringify(this.userdata));
localStorage.setItem("keys", JSON.stringify(keys));
return true; return true;
}catch (error) { }catch (error) {
return false; return false;
@@ -36,7 +43,18 @@ export class IServService {
} }
public async getKeys(): Promise<AuthKeys> { public async getKeys(): Promise<AuthKeys> {
return await firstValueFrom(this.client.post<AuthKeys>(this.backend + "/auth/login", this.userdata)); const keys = await firstValueFrom(this.client.post<AuthKeys>(this.backend + "/iserv/login", this.userdata));
localStorage.setItem("keys", JSON.stringify(keys));
return keys;
}
public async getGroups(): Promise<string[]> {
try {
return (await firstValueFrom(this.client.post<{value: string[]}>(this.backend + "/iserv/groups?domain=" + this.userdata.domain, this.keys))).value;
} catch {
await this.getKeys();
return (await firstValueFrom(this.client.post<{value: string[]}>(this.backend + "/iserv/groups?domain=" + this.userdata.domain, this.keys))).value;
}
} }
} }

View File

@@ -9,15 +9,24 @@ import {firstValueFrom} from "rxjs";
}) })
export class UnitsService { export class UnitsService {
private schools: {[domain: string]: {today: string, tomorrow: string}} = { public schools: {[domain: string]: {today: string, tomorrow: string, classes: string[]}} = {
["hgbp.de"]: { ["hgbp.de"]: {
today: "https://www.humboldt-gymnasium.de/vertretungsplan/PlanINet/heute/subst_001.htm", today: "https://www.humboldt-gymnasium.de/vertretungsplan/PlanINet/heute/subst_001.htm",
tomorrow: "https://www.humboldt-gymnasium.de/vertretungsplan/PlanINet/morgen/subst_001.htm" tomorrow: "https://www.humboldt-gymnasium.de/vertretungsplan/PlanINet/morgen/subst_001.htm",
classes: ["5a", "5b", "5c", "5d", "6a", "6b", "6c", "6d", "7a", "7b", "7c", "7d", "8a", "8b", "8c", "8d", "9a", "9b", "9c", "9d", "10a", "10b", "10c", "10d", "11a", "11b", "11c", "11d", "Q1", "Q2"]
} }
} }
constructor(private iserv: IServService, private client: HttpClient) {} constructor(private iserv: IServService, private client: HttpClient) {}
public doesSchoolExist(): boolean {
return this.schools[this.iserv.userdata.domain] != undefined;
}
public getClasses(): string[] {
return this.schools[this.iserv.userdata.domain]?.classes;
}
public async getSubstitutionPlan(date: "today" | "tomorrow"): Promise<UnitsData> { public async getSubstitutionPlan(date: "today" | "tomorrow"): Promise<UnitsData> {
if (this.schools[this.iserv.userdata.domain] == undefined) return undefined; if (this.schools[this.iserv.userdata.domain] == undefined) return undefined;
const url = this.schools[this.iserv.userdata.domain][date]; const url = this.schools[this.iserv.userdata.domain][date];

View File

@@ -1,9 +1,10 @@
export interface Substitution { export interface Substitution {
class: string; classes: string[];
times: number[]; times: number[];
type: string; type: string;
representative: string; representative: string;
lesson: string; lesson: string;
newLesson: string;
room: string; room: string;
teacher: string; teacher: string;
description: string; description: string;

View File

@@ -44,11 +44,13 @@
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-select label="Ordner" [value]="folders[0]" interface="action-sheet" (ionChange)="changeFolder(select.value)" #select> <ion-item style="--background: transparent; --border-color: transparent">
<ion-select-option *ngFor="let folder of folders" [value]="folder"> <ion-select label="Ordner" [value]="folders[0]" interface="action-sheet" (ionChange)="changeFolder(select.value)" #select>
{{folder.name}} <ion-select-option *ngFor="let folder of folders" [value]="folder">
</ion-select-option> {{folder.name}}
</ion-select> </ion-select-option>
</ion-select>
</ion-item>
<ion-list> <ion-list>
<ion-item *ngFor="let message of mails" class="mail pointer" (click)="selectMail(message, mailModal)"> <ion-item *ngFor="let message of mails" class="mail pointer" (click)="selectMail(message, mailModal)">

View File

@@ -3,15 +3,7 @@
<ion-buttons slot="start"> <ion-buttons slot="start">
<ion-menu-button></ion-menu-button> <ion-menu-button></ion-menu-button>
</ion-buttons> </ion-buttons>
<ion-title>Vertretungsplan</ion-title>
<ion-segment value="today" (ionChange)="changeDate(segment.value)" #segment>
<ion-segment-button value="today">
<ion-label>Heute</ion-label>
</ion-segment-button>
<ion-segment-button value="tomorrow">
<ion-label>Morgen</ion-label>
</ion-segment-button>
</ion-segment>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
@@ -22,9 +14,14 @@
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-select label="Klasse" [value]="currentClass" interface="action-sheet" (ionChange)="changeClass(select.value)" #select> <ion-segment value="today" (ionChange)="changeDate(segment.value)" #segment>
<ion-select-option *ngFor="let className of getDistinctClasses()" [value]="className" [innerHtml]="className" /> <ion-segment-button value="today">
</ion-select> <ion-label>Heute</ion-label>
</ion-segment-button>
<ion-segment-button value="tomorrow">
<ion-label>Morgen</ion-label>
</ion-segment-button>
</ion-segment>
<section *ngIf="showNews"> <section *ngIf="showNews">
<ion-card *ngFor="let info of data?.notifications"> <ion-card *ngFor="let info of data?.notifications">
@@ -35,16 +32,26 @@
</section> </section>
<section *ngIf="!showNews"> <section *ngIf="!showNews">
<ion-item style="--background: transparent; --border-color: transparent">
<ion-select label="Klasse" [value]="currentClass" interface="action-sheet" (ionChange)="changeClass(select.value)" #select>
<ion-select-option value="all">Alle Klassen</ion-select-option>
<ion-select-option *ngFor="let className of units.getClasses()" [value]="className" [innerHtml]="className" />
</ion-select>
</ion-item>
<ion-item style="--background: transparent; --border-color: transparent" *ngIf="currentClass == 'Q1' || currentClass == 'Q2'">
<ion-checkbox justify="space-between" [(ngModel)]="filterByClasses" (ionChange)="showOnlyCourses(classes.checked)" #classes>Nur eigene Kurse anzeigen</ion-checkbox>
</ion-item>
<ion-card <ion-card
*ngFor="let subs of data?.substitutions" *ngFor="let subs of data?.substitutions"
class="subs {{subs.type.replace(' ', '')}}" class="subs {{subs.type.replace(' ', '').replace('.', '')}}"
[ngClass]="{'hide': subs.class != currentClass && currentClass != undefined}" [ngClass]="{'hide': (subs.classes.indexOf(currentClass) == -1 && currentClass != 'all') || !hasClass(subs.lesson)}"
> >
<ion-card-content> <ion-card-content>
<ion-label class="times">{{subs.times.join(" - ")}}</ion-label> <ion-label class="times">{{subs.times.join(" - ")}}</ion-label>
<div> <div>
<ion-label class="type">{{subs.type}}</ion-label> <ion-label class="type">{{subs.type}}</ion-label>
<ion-label class="desc" [innerHtml]="subs.lesson + ' (' + subs.teacher + ') ' + subs.room + ' ' + subs.description"></ion-label> <ion-label class="desc" [innerHtml]="getDetails(subs)"></ion-label>
</div> </div>
</ion-card-content> </ion-card-content>
</ion-card> </ion-card>

View File

@@ -26,8 +26,8 @@
--background: var(--ion-color-success-shade); --background: var(--ion-color-success-shade);
} }
&.bittebeachten { &.bittebeachten, &.stregulärem {
--background: var(--ion-color-warning-shade); --background: var(--ion-color-tertiary-shade);
} }
&.Vertretung { &.Vertretung {
@@ -39,7 +39,7 @@
} }
&.Verlegung { &.Verlegung {
--background: var(--ion-color-secondary-shade); --background: var(--ion-color-warning-shade);
} }
&.hide { &.hide {

View File

@@ -1,9 +1,10 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular'; import {AlertController, IonicModule} from '@ionic/angular';
import {UnitsService} from "../../api/units.service"; import {UnitsService} from "../../api/units.service";
import {UnitsData} from "../../entities/substitution"; import {Substitution, UnitsData} from "../../entities/substitution";
import {IServService} from "../../api/iserv.service";
@Component({ @Component({
selector: 'app-substitution', selector: 'app-substitution',
@@ -16,36 +17,89 @@ export class SubstitutionPage implements OnInit {
public data: UnitsData; public data: UnitsData;
public showNews: boolean = false; public showNews: boolean = false;
public courses: string[] = [];
public currentClass: string; public currentClass: string;
public filterByClasses: boolean = false;
constructor(private units: UnitsService) { constructor(public units: UnitsService, private iserv: IServService, private alerts: AlertController) {
this.currentClass = localStorage.getItem("class"); this.currentClass = localStorage.getItem("class") || 'all';
this.filterByClasses = localStorage.getItem("filterByClasses") == "true";
} }
async ngOnInit() { async ngOnInit() {
if (!this.units.doesSchoolExist()) {
const alert = await this.alerts.create({
subHeader: "Fehler",
message: "Deine Schule wird nicht unterstützt!",
buttons: ["Ok"]
});
await alert.present();
return;
}
this.data = await this.units.getSubstitutionPlan("today"); this.data = await this.units.getSubstitutionPlan("today");
const groups = await this.iserv.getGroups();
for (let group of groups) {
if (!group.includes(".")) continue;
this.courses.push(group.split(".")[1]);
}
} }
public async changeDate(date: string) { public async changeDate(date: string) {
this.data = await this.units.getSubstitutionPlan(date as "today" | "tomorrow"); this.data = await this.units.getSubstitutionPlan(date as "today" | "tomorrow");
} }
public getDistinctClasses(): string[] {
const classes: string[] = [];
if (this.data == undefined) return [];
for (let subs of this.data.substitutions) {
if (classes.indexOf(subs.class) == -1) {
classes.push(subs.class)
}
}
return classes;
}
public changeClass(className: string) { public changeClass(className: string) {
this.currentClass = className; this.currentClass = className;
localStorage.setItem("class", className); localStorage.setItem("class", className);
this.filterByClasses = false;
this.showOnlyCourses(false);
}
public getDetails(subs: Substitution): string {
if (subs.type == "bitte beachten") {
const desc = subs.description != "&nbsp;" ? ' - ' + subs.description : "";
let info = `${subs.lesson} (${subs.teacher}) in ${subs.room}`;
if (subs.lesson != subs.newLesson) {
info = `${subs.newLesson} (${subs.representative}) statt ${subs.lesson} (${subs.teacher}) in ${subs.room}`;
}
return info + desc;
}
switch (subs.type) {
case "Vertretung":
case "st. regulärem Unt.":
return `${subs.lesson} (${subs.representative} statt ${subs.teacher}) in ${subs.room}`;
case "Raumtausch":
return `${subs.lesson} (${subs.teacher}) in ${subs.room}`;
case "Entfall":
return `${subs.lesson} (${subs.teacher})`;
case "Stillarbeit":
return `${subs.lesson} (${subs.teacher}) in ${subs.room}`;
case "Verlegung":
return `${subs.newLesson} (${subs.representative}) statt ${subs.lesson} (${subs.teacher}) in ${subs.room}`;
default:
return subs.lesson + ' (' + subs.teacher + ') ' + subs.room;
}
}
public showOnlyCourses(toggle: boolean) {
localStorage.setItem("filterByClasses", toggle.toString());
}
public hasClass(course: string): boolean {
if (!this.filterByClasses) return true;
return this.courses.includes(course);
} }
} }

View File

@@ -33,10 +33,6 @@ ion-menu-button {
cursor: pointer; cursor: pointer;
} }
ion-select {
padding-inline: 15px;
}
.active { .active {
color: var(--ion-color-primary); color: var(--ion-color-primary);
} }