Reworked login system + added substitution page
This commit is contained in:
6
BackendMetadata/exercises.txt
Normal file
6
BackendMetadata/exercises.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
GET https://hgbp.de/iserv/exercise.csv
|
||||||
|
Payload: HEADER(cookie)[IServSession={sessionKey}; IServSAT={satKey};]
|
||||||
|
|
||||||
|
ON_SUCCESS:
|
||||||
|
STATUS 200
|
||||||
|
CSV RESPONSE
|
||||||
@@ -2,5 +2,10 @@ POST https://hgbp.de/iserv/auth/login
|
|||||||
Payload: FORMDATA(_username: STRING, _password: STRING)
|
Payload: FORMDATA(_username: STRING, _password: STRING)
|
||||||
|
|
||||||
ON_SUCCESS:
|
ON_SUCCESS:
|
||||||
STATUS 302
|
|
||||||
SessionCookie: HEADER(set-cookie)[IServAuthSession={key};...]
|
FOLLOW REDIRECT
|
||||||
|
EXTRACT avatar URL
|
||||||
|
MAKE REQUEST
|
||||||
|
FOLLOW REDIRECTS
|
||||||
|
|
||||||
|
Tokens: HEADER(Set-Cookie)[IServSAT={sat-Key}; IServSession={sessionKey}]
|
||||||
|
|||||||
@@ -9,7 +9,9 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Aspose.Email" Version="23.3.0" />
|
<PackageReference Include="Aspose.Email" Version="23.3.0" />
|
||||||
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.46" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.3" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.3" />
|
||||||
|
<PackageReference Include="PuppeteerSharp" Version="9.1.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||||
<PackageReference Include="WebDav.Client" Version="2.8.0" />
|
<PackageReference Include="WebDav.Client" Version="2.8.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
50
BetterIServ.Backend/Controllers/AuthController.cs
Normal file
50
BetterIServ.Backend/Controllers/AuthController.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
using BetterIServ.Backend.Entities;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using PuppeteerSharp;
|
||||||
|
using Credentials = BetterIServ.Backend.Entities.Credentials;
|
||||||
|
|
||||||
|
namespace BetterIServ.Backend.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("auth")]
|
||||||
|
public class AuthController : ControllerBase {
|
||||||
|
|
||||||
|
[HttpPost("login")]
|
||||||
|
public async Task<ActionResult<AuthKeys>> GetAuthKeysV2([FromBody] Credentials credentials) {
|
||||||
|
await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true });
|
||||||
|
await using var page = await browser.NewPageAsync();
|
||||||
|
await page.GoToAsync($"https://{credentials.Domain}/iserv/auth/login");
|
||||||
|
|
||||||
|
await page.WaitForSelectorAsync("#form-username > input");
|
||||||
|
await page.Keyboard.TypeAsync(credentials.Username);
|
||||||
|
await page.Keyboard.PressAsync("Tab");
|
||||||
|
await page.Keyboard.TypeAsync(credentials.Password);
|
||||||
|
await page.ClickAsync("body > div > main > div > div.panel-body > form > div.row > div:nth-child(1) > button");
|
||||||
|
await Task.Delay(500);
|
||||||
|
|
||||||
|
var authKeys = new AuthKeys();
|
||||||
|
var cookies = await page.GetCookiesAsync();
|
||||||
|
foreach (var cookie in cookies) {
|
||||||
|
switch (cookie.Name) {
|
||||||
|
case "IServSession":
|
||||||
|
authKeys.Session = cookie.Value;
|
||||||
|
break;
|
||||||
|
case "IServSAT":
|
||||||
|
authKeys.Sat = cookie.Value;
|
||||||
|
break;
|
||||||
|
case "IServAuthSID":
|
||||||
|
authKeys.AuthSid = cookie.Value;
|
||||||
|
break;
|
||||||
|
case "IServSATId":
|
||||||
|
authKeys.SatId = cookie.Value;
|
||||||
|
break;
|
||||||
|
case "IServAuthSession":
|
||||||
|
authKeys.AuthSession = cookie.Value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return authKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace BetterIServ.Backend.Controllers;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
public class HelperController : ControllerBase {
|
|
||||||
|
|
||||||
[HttpPost("/login")]
|
|
||||||
public async Task<ActionResult<string>> Login([FromForm] string email, [FromForm] string password) {
|
|
||||||
try {
|
|
||||||
using var client = new HttpClient(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, AllowAutoRedirect = false }) { Timeout = TimeSpan.FromSeconds(5) };
|
|
||||||
|
|
||||||
var split = email.Split("@");
|
|
||||||
var username = split[0];
|
|
||||||
var domain = split[1];
|
|
||||||
|
|
||||||
var form = new FormUrlEncodedContent(new[] {
|
|
||||||
new KeyValuePair<string, string>("_username", username),
|
|
||||||
new KeyValuePair<string, string>("_password", password)
|
|
||||||
});
|
|
||||||
|
|
||||||
var request = new HttpRequestMessage {
|
|
||||||
RequestUri = new Uri($"https://{domain}/iserv/auth/login"),
|
|
||||||
Method = HttpMethod.Post,
|
|
||||||
Content = form
|
|
||||||
};
|
|
||||||
|
|
||||||
var response = await client.SendAsync(request);
|
|
||||||
var header = response.Headers.GetValues("Set-Cookie").First();
|
|
||||||
var part = header.Split(";")[0];
|
|
||||||
|
|
||||||
if (!part.Contains("IServAuthSession")) throw new Exception();
|
|
||||||
|
|
||||||
return Ok(part.Replace("IServAuthSession=", ""));
|
|
||||||
}
|
|
||||||
catch (Exception) {
|
|
||||||
return Unauthorized();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
61
BetterIServ.Backend/Controllers/UnitsController.cs
Normal file
61
BetterIServ.Backend/Controllers/UnitsController.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using System.Text;
|
||||||
|
using BetterIServ.Backend.Entities;
|
||||||
|
using HtmlAgilityPack;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace BetterIServ.Backend.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("units")]
|
||||||
|
public class UnitsController : ControllerBase {
|
||||||
|
|
||||||
|
[HttpGet("substitution")]
|
||||||
|
public async Task<ActionResult<UnitsData>> GetToday([FromQuery] string url) {
|
||||||
|
var client = new HttpClient();
|
||||||
|
var buffer = await client.GetByteArrayAsync(url);
|
||||||
|
var raw = Encoding.GetEncoding("ISO-8859-1").GetString(buffer);
|
||||||
|
var html = new HtmlDocument();
|
||||||
|
html.LoadHtml(raw);
|
||||||
|
|
||||||
|
var data = new UnitsData {
|
||||||
|
Notifications = new List<string>(),
|
||||||
|
Substitutions = new List<Substitution>()
|
||||||
|
};
|
||||||
|
|
||||||
|
var info = html.DocumentNode.SelectSingleNode("//body/center[1]/table");
|
||||||
|
for (int i = 2; i < info.ChildNodes.Count; i++) {
|
||||||
|
var notification = info.ChildNodes[i];
|
||||||
|
if (notification.ChildNodes.Count == 2) {
|
||||||
|
data.Notifications.Add(
|
||||||
|
notification.ChildNodes[0].InnerText +
|
||||||
|
notification.ChildNodes[1].InnerText
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (notification.ChildNodes.Count == 1) {
|
||||||
|
data.Notifications.Add(notification.ChildNodes[0].InnerText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var substitutions = html.DocumentNode.SelectNodes("//body/center[1]")[0].ChildNodes[6];
|
||||||
|
for (int i = 4; i < substitutions.ChildNodes.Count; i++) {
|
||||||
|
var node = substitutions.ChildNodes[i];
|
||||||
|
if (node.ChildNodes.Count < 9) continue;
|
||||||
|
|
||||||
|
var substitution = new Substitution {
|
||||||
|
Class = node.ChildNodes[0].InnerText,
|
||||||
|
Times = node.ChildNodes[1].InnerText.Split(" - ").Select(int.Parse).ToArray(),
|
||||||
|
Type = node.ChildNodes[2].InnerText,
|
||||||
|
Representative = node.ChildNodes[3].InnerText,
|
||||||
|
Lesson = node.ChildNodes[4].InnerText,
|
||||||
|
Room = node.ChildNodes[6].InnerText,
|
||||||
|
Teacher = node.ChildNodes[7].InnerText,
|
||||||
|
Description = node.ChildNodes[9].InnerText
|
||||||
|
};
|
||||||
|
|
||||||
|
data.Substitutions.Add(substitution);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
9
BetterIServ.Backend/Entities/AuthKeys.cs
Normal file
9
BetterIServ.Backend/Entities/AuthKeys.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace BetterIServ.Backend.Entities;
|
||||||
|
|
||||||
|
public struct AuthKeys {
|
||||||
|
public string Session { get; set; }
|
||||||
|
public string Sat { get; set; }
|
||||||
|
public string AuthSid { get; set; }
|
||||||
|
public string SatId { get; set; }
|
||||||
|
public string AuthSession { get; set; }
|
||||||
|
}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
namespace BetterIServ.Backend.Entities;
|
namespace BetterIServ.Backend.Entities;
|
||||||
|
|
||||||
public class Credentials {
|
public class Credentials {
|
||||||
public string? Domain { get; set; }
|
public required string Domain { get; set; }
|
||||||
public string? Username { get; set; }
|
public required string Username { get; set; }
|
||||||
public string? Password { get; set; }
|
public required string Password { get; set; }
|
||||||
public string? Token { get; set; }
|
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
public sealed class MailData : Credentials {
|
public sealed class MailData : Credentials {
|
||||||
|
|
||||||
public string? MailBody { get; set; }
|
public required string MailBody { get; set; }
|
||||||
public string? Receiver { get; set; }
|
public required string Receiver { get; set; }
|
||||||
public string? Subject { get; set; }
|
public required string Subject { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
namespace BetterIServ.Backend.Entities;
|
namespace BetterIServ.Backend.Entities;
|
||||||
|
|
||||||
public class SingleResult<TValue> {
|
public struct SingleResult<TValue> {
|
||||||
public TValue? Value { get; set; }
|
public TValue? Value { get; set; }
|
||||||
}
|
}
|
||||||
12
BetterIServ.Backend/Entities/Substitution.cs
Normal file
12
BetterIServ.Backend/Entities/Substitution.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace BetterIServ.Backend.Entities;
|
||||||
|
|
||||||
|
public struct Substitution {
|
||||||
|
public string Class { get; set; }
|
||||||
|
public int[] Times { get; set; }
|
||||||
|
public string Type { get; set; }
|
||||||
|
public string Representative { get; set; }
|
||||||
|
public string Lesson { get; set; }
|
||||||
|
public string Room { get; set; }
|
||||||
|
public string Teacher { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
}
|
||||||
6
BetterIServ.Backend/Entities/UnitsData.cs
Normal file
6
BetterIServ.Backend/Entities/UnitsData.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace BetterIServ.Backend.Entities;
|
||||||
|
|
||||||
|
public struct UnitsData {
|
||||||
|
public IList<string> Notifications { get; set; }
|
||||||
|
public IList<Substitution> Substitutions { get; set; }
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Cors.Infrastructure;
|
using PuppeteerSharp;
|
||||||
|
|
||||||
|
await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultChromiumRevision);
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import {HttpClient} from "@angular/common/http";
|
import {HttpClient} from "@angular/common/http";
|
||||||
import {Userdata} from "../entities/userdata";
|
import {Userdata, AuthKeys} from "../entities/userdata";
|
||||||
import {firstValueFrom} from "rxjs";
|
import {firstValueFrom} from "rxjs";
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@@ -20,17 +20,14 @@ export class IServService {
|
|||||||
|
|
||||||
public async login(email: string, password: string): Promise<boolean> {
|
public async login(email: string, password: string): Promise<boolean> {
|
||||||
const split = email.split('@');
|
const split = email.split('@');
|
||||||
this.userdata = {};
|
this.userdata = {
|
||||||
this.userdata.username = split[0];
|
username: split[0],
|
||||||
this.userdata.domain = split[1];
|
domain: split[1],
|
||||||
this.userdata.password = password;
|
password
|
||||||
|
};
|
||||||
const data = new FormData();
|
|
||||||
data.append("email", email);
|
|
||||||
data.append("password", password);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.userdata.token = await firstValueFrom(this.client.post(`${this.backend}/login`, data, {responseType: "text"}));
|
await firstValueFrom(this.client.post(this.backend + "/auth/login", this.userdata));
|
||||||
localStorage.setItem("userdata", JSON.stringify(this.userdata));
|
localStorage.setItem("userdata", JSON.stringify(this.userdata));
|
||||||
return true;
|
return true;
|
||||||
}catch (error) {
|
}catch (error) {
|
||||||
@@ -38,4 +35,8 @@ export class IServService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getKeys(): Promise<AuthKeys> {
|
||||||
|
return await firstValueFrom(this.client.post<AuthKeys>(this.backend + "/auth/login", this.userdata));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export class MailService {
|
|||||||
domain: this.iserv.userdata.domain,
|
domain: this.iserv.userdata.domain,
|
||||||
username: this.iserv.userdata.username,
|
username: this.iserv.userdata.username,
|
||||||
password: this.iserv.userdata.password,
|
password: this.iserv.userdata.password,
|
||||||
token: this.iserv.userdata.token,
|
|
||||||
subject, receiver, mailBody
|
subject, receiver, mailBody
|
||||||
};
|
};
|
||||||
await firstValueFrom(this.client.post(this.iserv.backend + "/mail/send", data));
|
await firstValueFrom(this.client.post(this.iserv.backend + "/mail/send", data));
|
||||||
|
|||||||
27
BetterIServ.Mobile/src/app/api/units.service.ts
Normal file
27
BetterIServ.Mobile/src/app/api/units.service.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {HttpClient} from "@angular/common/http";
|
||||||
|
import {IServService} from "./iserv.service";
|
||||||
|
import {UnitsData} from "../entities/substitution";
|
||||||
|
import {firstValueFrom} from "rxjs";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class UnitsService {
|
||||||
|
|
||||||
|
private schools: {[domain: string]: {today: string, tomorrow: string}} = {
|
||||||
|
["hgbp.de"]: {
|
||||||
|
today: "https://www.humboldt-gymnasium.de/vertretungsplan/PlanINet/heute/subst_001.htm",
|
||||||
|
tomorrow: "https://www.humboldt-gymnasium.de/vertretungsplan/PlanINet/morgen/subst_001.htm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private iserv: IServService, private client: HttpClient) {}
|
||||||
|
|
||||||
|
public async getSubstitutionPlan(date: "today" | "tomorrow"): Promise<UnitsData> {
|
||||||
|
if (this.schools[this.iserv.userdata.domain] == undefined) return undefined;
|
||||||
|
const url = this.schools[this.iserv.userdata.domain][date];
|
||||||
|
return await firstValueFrom(this.client.get<UnitsData>(this.iserv.backend + "/units/substitution?url=" + url));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -22,4 +22,8 @@ export const routes: Routes = [
|
|||||||
path: 'mails',
|
path: 'mails',
|
||||||
loadComponent: () => import('./pages/mails/mails.page').then( m => m.MailsPage)
|
loadComponent: () => import('./pages/mails/mails.page').then( m => m.MailsPage)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'substitution',
|
||||||
|
loadComponent: () => import('./pages/substitution/substitution.page').then( m => m.SubstitutionPage)
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
15
BetterIServ.Mobile/src/app/entities/substitution.ts
Normal file
15
BetterIServ.Mobile/src/app/entities/substitution.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export interface Substitution {
|
||||||
|
class: string;
|
||||||
|
times: number[];
|
||||||
|
type: string;
|
||||||
|
representative: string;
|
||||||
|
lesson: string;
|
||||||
|
room: string;
|
||||||
|
teacher: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnitsData {
|
||||||
|
notifications: string[];
|
||||||
|
substitutions: Substitution[];
|
||||||
|
}
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
export interface Userdata {
|
export interface Userdata {
|
||||||
domain?: string,
|
domain: string;
|
||||||
username?: string,
|
username: string;
|
||||||
password?: string,
|
password: string;
|
||||||
token?: string
|
}
|
||||||
|
|
||||||
|
export interface AuthKeys {
|
||||||
|
session: string;
|
||||||
|
sat: string;
|
||||||
|
authSid: string;
|
||||||
|
satId: string;
|
||||||
|
authSession: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,3 @@ ion-label {
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.active {
|
|
||||||
color: var(--ion-color-primary);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ export class LoginPage implements OnInit {
|
|||||||
|
|
||||||
await alert.present();
|
await alert.present();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(this.iservApi.userdata);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
ion-select {
|
|
||||||
padding-inline: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mail > div {
|
.mail > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<ion-header [translucent]="true">
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-menu-button></ion-menu-button>
|
||||||
|
</ion-buttons>
|
||||||
|
|
||||||
|
<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-header>
|
||||||
|
|
||||||
|
<ion-content [fullscreen]="true">
|
||||||
|
<ion-header collapse="condense">
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title size="large">Vertretungsplan</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
|
||||||
|
<ion-select label="Klasse" [value]="currentClass" interface="action-sheet" (ionChange)="changeClass(select.value)" #select>
|
||||||
|
<ion-select-option *ngFor="let className of getDistinctClasses()" [value]="className" [innerHtml]="className" />
|
||||||
|
</ion-select>
|
||||||
|
|
||||||
|
<section *ngIf="showNews">
|
||||||
|
<ion-card *ngFor="let info of data?.notifications">
|
||||||
|
<ion-card-content>
|
||||||
|
<ion-label [innerHtml]="info" style="color: #FFF"/>
|
||||||
|
</ion-card-content>
|
||||||
|
</ion-card>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section *ngIf="!showNews">
|
||||||
|
<ion-card
|
||||||
|
*ngFor="let subs of data?.substitutions"
|
||||||
|
class="subs {{subs.type.replace(' ', '')}}"
|
||||||
|
[ngClass]="{'hide': subs.class != currentClass && currentClass != undefined}"
|
||||||
|
>
|
||||||
|
<ion-card-content>
|
||||||
|
<ion-label class="times">{{subs.times.join(" - ")}}</ion-label>
|
||||||
|
<div>
|
||||||
|
<ion-label class="type">{{subs.type}}</ion-label>
|
||||||
|
<ion-label class="desc" [innerHtml]="subs.lesson + ' (' + subs.teacher + ') ' + subs.room + ' ' + subs.description"></ion-label>
|
||||||
|
</div>
|
||||||
|
</ion-card-content>
|
||||||
|
</ion-card>
|
||||||
|
</section>
|
||||||
|
</ion-content>
|
||||||
|
|
||||||
|
<ion-footer>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-grid>
|
||||||
|
<ion-row>
|
||||||
|
<ion-col>
|
||||||
|
<ion-tab-button (click)="showNews = false" [ngClass]="{'active': !showNews}">
|
||||||
|
<ion-icon ios="list-outline" md="list-sharp" />
|
||||||
|
Vertretungen
|
||||||
|
</ion-tab-button>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col>
|
||||||
|
<ion-tab-button (click)="showNews = true" [ngClass]="{'active': showNews}">
|
||||||
|
<ion-icon ios="newspaper-outline" md="newspaper-sharp" />
|
||||||
|
Nachrichten
|
||||||
|
</ion-tab-button>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-grid>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-footer>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
.subs {
|
||||||
|
ion-card-content {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
color: #FFF;
|
||||||
|
|
||||||
|
.times {
|
||||||
|
font-size: 25px;
|
||||||
|
line-height: 25px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: end;
|
||||||
|
|
||||||
|
.type {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.Raumtausch {
|
||||||
|
--background: var(--ion-color-success-shade);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bittebeachten {
|
||||||
|
--background: var(--ion-color-warning-shade);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.Vertretung {
|
||||||
|
--background: var(--ion-color-primary-shade);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.Entfall, &.Stillarbeit {
|
||||||
|
--background: var(--ion-color-danger-shade);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.Verlegung {
|
||||||
|
--background: var(--ion-color-secondary-shade);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { IonicModule } from '@ionic/angular';
|
||||||
|
import {UnitsService} from "../../api/units.service";
|
||||||
|
import {UnitsData} from "../../entities/substitution";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-substitution',
|
||||||
|
templateUrl: './substitution.page.html',
|
||||||
|
styleUrls: ['./substitution.page.scss'],
|
||||||
|
standalone: true,
|
||||||
|
imports: [IonicModule, CommonModule, FormsModule]
|
||||||
|
})
|
||||||
|
export class SubstitutionPage implements OnInit {
|
||||||
|
|
||||||
|
public data: UnitsData;
|
||||||
|
public showNews: boolean = false;
|
||||||
|
public currentClass: string;
|
||||||
|
|
||||||
|
constructor(private units: UnitsService) {
|
||||||
|
this.currentClass = localStorage.getItem("class");
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.data = await this.units.getSubstitutionPlan("today");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async changeDate(date: string) {
|
||||||
|
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) {
|
||||||
|
this.currentClass = className;
|
||||||
|
localStorage.setItem("class", className);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -32,3 +32,11 @@ ion-menu-button {
|
|||||||
.pointer {
|
.pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ion-select {
|
||||||
|
padding-inline: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
color: var(--ion-color-primary);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user