Merge pull request 'Dependency Day + Basic Setup' (#11) from dependency-day-basic-setup into main

Reviewed-on: #11
This commit is contained in:
it-as 2024-03-12 17:40:20 +01:00
commit 546fdc4435
53 changed files with 21703 additions and 8094 deletions

41
frontend/.eslintrc.json Normal file
View File

@ -0,0 +1,41 @@
{
"root": true,
"ignorePatterns": ["projects/**/*"],
"overrides": [
{
"files": ["*.ts"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "li",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "li",
"style": "kebab-case"
}
]
}
},
{
"files": ["*.html"],
"extends": [
"plugin:@angular-eslint/template/recommended",
"plugin:@angular-eslint/template/accessibility"
],
"rules": {}
}
]
}

0
frontend/.prettierignore Normal file
View File

11
frontend/.prettierrc.json Normal file
View File

@ -0,0 +1,11 @@
{
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"semi": true,
"bracketSpacing": true,
"arrowParens": "avoid",
"trailingComma": "es5",
"bracketSameLine": true,
"printWidth": 80
}

View File

@ -26,13 +26,8 @@
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.scss"],
"scripts": []
},
"configurations": {
@ -72,10 +67,10 @@
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "li-dance-backoffice:build:production"
"buildTarget": "li-dance-backoffice:build:production"
},
"development": {
"browserTarget": "li-dance-backoffice:build:development"
"buildTarget": "li-dance-backoffice:build:development"
}
},
"defaultConfiguration": "development"
@ -83,29 +78,19 @@
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "li-dance-backoffice:build"
"buildTarget": "li-dance-backoffice:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
}
}
}
}
},
"defaultProject": "li-dance-backoffice"
"cli": {
"schematicCollections": ["@angular-eslint/schematics"]
}
}

View File

@ -1,44 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/li-dance-backoffice'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

28215
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,38 +6,66 @@
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "ng lint",
"foramt": "npx prettier --write 'src/**/*.ts' 'src/**/*.html' 'src/**/*.scss'"
},
"private": true,
"dependencies": {
"@angular/animations": "~13.1.0",
"@angular/cdk": "^13.1.0",
"@angular/common": "~13.1.0",
"@angular/compiler": "~13.1.0",
"@angular/core": "~13.1.0",
"@angular/forms": "~13.1.0",
"@angular/material": "^13.1.0",
"@angular/platform-browser": "~13.1.0",
"@angular/platform-browser-dynamic": "~13.1.0",
"@angular/router": "~13.1.0",
"bootstrap": "^5.2.2",
"bootstrap-icons": "^1.9.1",
"@angular/animations": "^17.2.4",
"@angular/cdk": "^17.2.2",
"@angular/common": "^17.2.4",
"@angular/compiler": "^17.2.4",
"@angular/core": "^17.2.4",
"@angular/forms": "^17.2.4",
"@angular/material": "^17.2.2",
"@angular/platform-browser": "^17.2.4",
"@angular/platform-browser-dynamic": "^17.2.4",
"@angular/router": "^17.2.4",
"bootstrap": "^5.3.3",
"bootstrap-icons": "^1.11.3",
"rxjs": "~7.4.0",
"tslib": "^2.3.0",
"zone.js": "~0.11.4"
"zone.js": "~0.14.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "~13.1.3",
"@angular/cli": "~13.1.3",
"@angular/compiler-cli": "~13.1.0",
"@types/jasmine": "~3.10.0",
"@angular-devkit/build-angular": "^17.2.3",
"@angular-eslint/builder": "17.2.1",
"@angular-eslint/eslint-plugin": "17.2.1",
"@angular-eslint/eslint-plugin-template": "17.2.1",
"@angular-eslint/schematics": "17.2.1",
"@angular-eslint/template-parser": "17.2.1",
"@angular/cli": "^17.2.3",
"@angular/compiler-cli": "^17.2.4",
"@types/jest": "^29.5.12",
"@types/node": "^12.11.1",
"jasmine-core": "~3.10.0",
"karma": "~6.3.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.1.0",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "~1.7.0",
"typescript": "~4.5.2"
"@typescript-eslint/eslint-plugin": "6.19.0",
"@typescript-eslint/parser": "6.19.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"jest": "^29.7.0",
"jest-preset-angular": "^14.0.3",
"prettier": "^3.2.5",
"prettier-eslint": "^16.3.0",
"typescript": "~5.3.3"
},
"jest": {
"preset": "jest-preset-angular",
"setupFilesAfterEnv": [
"<rootDir>/src/setup.jest.ts"
],
"testPathIgnorePatterns": [
"<rootDir>/node_modules/",
"<rootDir>/dist/"
],
"globals": {
"ts-jest": {
"tsConfig": "<rootDir>/tsconfig.spec.json",
"stringifyContentPathRegex": "\\.html$"
}
}
}
}

View File

@ -9,11 +9,11 @@ const routes: Routes = [
{ path: 'visits', component: VisitsComponent },
{ path: 'select', component: VisitsDatetimeComponent },
{ path: 'visits/:date/:time', component: VisitsComponent },
{ path: '**', redirectTo: 'students' }
{ path: '**', redirectTo: 'students' },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
exports: [RouterModule],
})
export class AppRoutingModule { }
export class AppRoutingModule {}

View File

@ -5,12 +5,8 @@ import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
imports: [RouterTestingModule],
declarations: [AppComponent],
}).compileComponents();
});
@ -30,6 +26,8 @@ describe('AppComponent', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.content span')?.textContent).toContain('li-dance-backoffice app is running!');
expect(compiled.querySelector('.content span')?.textContent).toContain(
'li-dance-backoffice app is running!'
);
});
});

View File

@ -3,7 +3,7 @@ import { Component } from '@angular/core';
@Component({
selector: 'li-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
title = 'li-dance-backoffice';

View File

@ -32,7 +32,7 @@ import { MatNativeDateModule, MatRippleModule } from '@angular/material/core';
EnrollPipe,
VisitsComponent,
StudentEnrollComponent,
VisitsDatetimeComponent
VisitsDatetimeComponent,
],
imports: [
BrowserModule,
@ -49,6 +49,6 @@ import { MatNativeDateModule, MatRippleModule } from '@angular/material/core';
MatNativeDateModule,
],
providers: [],
bootstrap: [AppComponent]
bootstrap: [AppComponent],
})
export class AppModule { }
export class AppModule {}

View File

@ -1,44 +1,117 @@
<div id="student-edit-overlay" [class]="visibility()">
<div id="student-edit-container">
<form id="student-edit-form">
<label id="student-edit-firstname-label" for="student-edit-firstname-input">Vorname</label>
<input id="student-edit-firstname-input" name="student-edit-firstname-input" type="text" [(ngModel)]="model.firstname" />
<label
id="student-edit-firstname-label"
for="student-edit-firstname-input"
>Vorname</label
>
<input
id="student-edit-firstname-input"
name="student-edit-firstname-input"
type="text"
[(ngModel)]="model.firstname" />
<label id="student-edit-lastname-label" for="student-edit-lastname-input">Nachname</label>
<input id="student-edit-lastname-input" name="student-edit-lastname-input" type="text" [(ngModel)]="model.lastname" />
<label id="student-edit-lastname-label" for="student-edit-lastname-input"
>Nachname</label
>
<input
id="student-edit-lastname-input"
name="student-edit-lastname-input"
type="text"
[(ngModel)]="model.lastname" />
<ng-container *ngIf="!simple">
<label id="student-edit-birthday-label" for="student-edit-birthday-input">Geburtsdatum</label>
<input id="student-edit-birthday-input" name="student-edit-birthday-input" type="date" [(ngModel)]="model.birthday" />
<label
id="student-edit-birthday-label"
for="student-edit-birthday-input"
>Geburtsdatum</label
>
<input
id="student-edit-birthday-input"
name="student-edit-birthday-input"
type="date"
[(ngModel)]="model.birthday" />
<label id="student-edit-gender-label" for="student-edit-gender-input">Geschlecht</label>
<select id="student-edit-gender-input" name="student-edit-gender-input" [(ngModel)]="model.gender" >
<option [value]="gender.id" *ngFor="let gender of genders">{{gender.text}}</option>
<label id="student-edit-gender-label" for="student-edit-gender-input"
>Geschlecht</label
>
<select
id="student-edit-gender-input"
name="student-edit-gender-input"
[(ngModel)]="model.gender">
<option [value]="gender.id" *ngFor="let gender of genders">
{{ gender.text }}
</option>
</select>
<label id="student-edit-street-label" for="student-edit-street-input">Strasse</label>
<input id="student-edit-street-input" name="student-edit-street-input" type="text" [(ngModel)]="model.street" />
<label id="student-edit-street-label" for="student-edit-street-input"
>Strasse</label
>
<input
id="student-edit-street-input"
name="student-edit-street-input"
type="text"
[(ngModel)]="model.street" />
<label id="student-edit-house-label" for="student-edit-house-input">Hausnummer</label>
<input id="student-edit-house-input" name="student-edit-house-input" type="text" [(ngModel)]="model.house" />
<label id="student-edit-house-label" for="student-edit-house-input"
>Hausnummer</label
>
<input
id="student-edit-house-input"
name="student-edit-house-input"
type="text"
[(ngModel)]="model.house" />
<label id="student-edit-housesuffix-label" for="student-edit-housesuffix-input">Suffix</label>
<input id="student-edit-housesuffix-input" name="student-edit-housesuffix-input" type="text" [(ngModel)]="model.house_suffix" />
<label
id="student-edit-housesuffix-label"
for="student-edit-housesuffix-input"
>Suffix</label
>
<input
id="student-edit-housesuffix-input"
name="student-edit-housesuffix-input"
type="text"
[(ngModel)]="model.house_suffix" />
<label id="student-edit-zip-label" for="student-edit-zip-input">PLZ</label>
<input id="student-edit-zip-input" name="student-edit-zip-input" type="text" [(ngModel)]="model.zip" />
<label id="student-edit-zip-label" for="student-edit-zip-input"
>PLZ</label
>
<input
id="student-edit-zip-input"
name="student-edit-zip-input"
type="text"
[(ngModel)]="model.zip" />
<label id="student-edit-city-label" for="student-edit-city-input">Stadt</label>
<input id="student-edit-city-input" name="student-edit-city-input" type="text" [(ngModel)]="model.city" />
<label id="student-edit-city-label" for="student-edit-city-input"
>Stadt</label
>
<input
id="student-edit-city-input"
name="student-edit-city-input"
type="text"
[(ngModel)]="model.city" />
<label id="student-edit-phone-label" for="student-edit-phone-input">Telefon</label>
<input id="student-edit-phone-input" name="student-edit-phone-input" type="text" [(ngModel)]="model.phone" />
<label id="student-edit-phone-label" for="student-edit-phone-input"
>Telefon</label
>
<input
id="student-edit-phone-input"
name="student-edit-phone-input"
type="text"
[(ngModel)]="model.phone" />
<label id="student-edit-email-label" for="student-edit-email-input">E-Mail</label>
<input id="student-edit-email-input" name="student-edit-email-input" type="text" [(ngModel)]="model.email" />
<label id="student-edit-email-label" for="student-edit-email-input"
>E-Mail</label
>
<input
id="student-edit-email-input"
name="student-edit-email-input"
type="text"
[(ngModel)]="model.email" />
</ng-container>
</form>
<div style="text-align:center;">
<div style="text-align: center">
<button class="button-save" (click)="save()">Save</button>
<button class="button-close" (click)="close()">Close</button>
</div>

View File

@ -1,8 +1,8 @@
.hidden{
display:none;
.hidden {
display: none;
}
.show{
display:grid;
.show {
display: grid;
}
#student-edit-container {
@ -21,7 +21,7 @@
inset: 0;
z-index: 99;
background: rgba(0,0,0,0.8);
background: rgba(0, 0, 0, 0.8);
grid-template-columns: 1fr 1fr 1fr;
}
@ -59,16 +59,16 @@ button.button-close {
}
label {
display:block;
width:100%;
display: block;
width: 100%;
margin-bottom: 0.5em;
}
select,
input[type=date],
input[type=text]{
display:block;
width:100%;
input[type='date'],
input[type='text'] {
display: block;
width: 100%;
height: 2em;
margin-bottom: 1.5em;
font-size: 1em;

View File

@ -5,10 +5,9 @@ import { StudentsService } from 'src/app/services/students/students.service';
@Component({
selector: 'li-student-edit',
templateUrl: './student-edit.component.html',
styleUrls: ['./student-edit.component.scss']
styleUrls: ['./student-edit.component.scss'],
})
export class StudentEditComponent implements OnInit {
@Input()
public student?: Student = undefined;
@ -19,27 +18,28 @@ export class StudentEditComponent implements OnInit {
public closing = new EventEmitter();
public genders = [
{id: 0, text: 'Männlich'},
{id: 1, text: 'Weiblich'},
{id: 2, text: 'Divers'},
{ id: 0, text: 'Männlich' },
{ id: 1, text: 'Weiblich' },
{ id: 2, text: 'Divers' },
];
public get model(): Student {
return this.student || <Student>{};
}
constructor(private studentsService: StudentsService) { }
constructor(private studentsService: StudentsService) {}
ngOnInit(): void {
}
ngOnInit(): void {}
public close(): void {
this.closing.emit();
}
public save(): void {
if(this.student){
this.studentsService.set(this.sanitize(this.model)).subscribe(_ => this.closing.emit());
if (this.student) {
this.studentsService
.set(this.sanitize(this.model))
.subscribe(_ => this.closing.emit());
} else {
this.closing.emit();
}
@ -50,7 +50,7 @@ export class StudentEditComponent implements OnInit {
}
private sanitize(student: Student): Student {
if(student){
if (student) {
student.firstname ??= '';
student.lastname ??= '';
student.gender ??= 0;

View File

@ -1,26 +1,47 @@
<div id="student-enroll-overlay" [class]="visibility()">
<div id="student-enroll-container">
<h1>{{model | name}}</h1>
<h1>{{ model | name }}</h1>
<form id="student-enroll-form">
<div style="display: grid; grid-template-columns: auto auto auto auto;">
<ng-container *ngFor="let enrollment of student?.enrollments; index as i">
<div style="display: grid; grid-template-columns: auto auto auto auto">
<ng-container
*ngFor="let enrollment of student?.enrollments; index as i">
<div class="enrollment-name">{{ enrollment.name }}</div>
<div>
<input *ngIf="enrollment.begin" id="student-enroll-begin-input{{i}}" name="student-enroll-begin-input{{i}}" type="date" [(ngModel)]="enrollment.begin" />
<button *ngIf="!enrollment.begin" class="button-add" (click)="enroll(enrollment)">+</button>
<input
*ngIf="enrollment.begin"
id="student-enroll-begin-input{{ i }}"
name="student-enroll-begin-input{{ i }}"
type="date"
[(ngModel)]="enrollment.begin" />
<button
*ngIf="!enrollment.begin"
class="button-add"
(click)="enroll(enrollment)">
+
</button>
</div>
<div>
<input *ngIf="enrollment.end" id="student-enroll-end-input{{i}}" name="student-enroll-end-input{{i}}" type="date" [(ngModel)]="enrollment.end" />
<input
*ngIf="enrollment.end"
id="student-enroll-end-input{{ i }}"
name="student-enroll-end-input{{ i }}"
type="date"
[(ngModel)]="enrollment.end" />
</div>
<div>
<button *ngIf="enrollment.begin" class="button-remove" (click)="deroll(enrollment)">X</button>
<button
*ngIf="enrollment.begin"
class="button-remove"
(click)="deroll(enrollment)">
X
</button>
</div>
</ng-container>
</div>
</form>
<div style="text-align:center;">
<div style="text-align: center">
<button class="button-save" (click)="save()">Save</button>
<button class="button-close" (click)="close()">Close</button>
</div>

View File

@ -1,8 +1,8 @@
.hidden{
display:none;
.hidden {
display: none;
}
.show{
display:grid;
.show {
display: grid;
}
#student-enroll-container {
@ -21,7 +21,7 @@
inset: 0;
z-index: 99;
background: rgba(0,0,0,0.8);
background: rgba(0, 0, 0, 0.8);
grid-template-columns: 1fr 1fr 1fr;
}

View File

@ -7,25 +7,22 @@ import { EnrollService } from 'src/app/services/enroll/enroll.service';
@Component({
selector: 'li-student-enroll',
templateUrl: './student-enroll.component.html',
styleUrls: ['./student-enroll.component.scss']
styleUrls: ['./student-enroll.component.scss'],
})
export class StudentEnrollComponent implements OnInit {
@Input()
public student?: Student;
@Output()
public closing = new EventEmitter();
public get model(): Student {
return this.student || <Student>{};
}
public constructor(private enrollService: EnrollService) { }
public constructor(private enrollService: EnrollService) {}
public ngOnInit() {
}
public ngOnInit() {}
public enroll(enrollment: StudentEnrollment) {
enrollment.begin = formatDate(new Date(), 'yyyy-MM-dd', 'en-US');
@ -47,9 +44,11 @@ export class StudentEnrollComponent implements OnInit {
return;
}
this.enrollService.set(
this.enrollService
.set(
this.student.sid,
this.student.enrollments.filter(e => e.begin))
this.student.enrollments.filter(e => e.begin)
)
.subscribe(_ => this.closing.emit());
}

View File

@ -2,10 +2,9 @@
<div class="grid-item">
<button class="button-add" (click)="add()">Hinzufügen</button>
</div>
<div class="grid-item">
</div>
<div class="grid-item"></div>
<div class="grid-item-full">
<div *ngIf="loading;else table">
<div *ngIf="loading; else table">
<mat-spinner class="center"></mat-spinner>
</div>
@ -13,55 +12,101 @@
<mat-table [dataSource]="dataSource">
<ng-container matColumnDef="Name">
<mat-header-cell *matHeaderCellDef> Name </mat-header-cell>
<mat-cell *matCellDef="let element" (click)="edit(element)">{{element | name}}</mat-cell>
<mat-cell *matCellDef="let element" (click)="edit(element)">{{
element | name
}}</mat-cell>
</ng-container>
<ng-container matColumnDef="Enroll">
<mat-header-cell *matHeaderCellDef> Gruppen </mat-header-cell>
<mat-cell *matCellDef="let element" (click)="enroll(element)">{{element | enroll}}</mat-cell>
<mat-cell *matCellDef="let element" (click)="enroll(element)">{{
element | enroll
}}</mat-cell>
</ng-container>
<ng-container matColumnDef="Birthday">
<mat-header-cell *matHeaderCellDef> Geburtsdatum </mat-header-cell>
<mat-cell *matCellDef="let element" (click)="edit(element)">{{element.birthday | date: 'dd.MM.yyyy'}}</mat-cell>
<mat-cell *matCellDef="let element" (click)="edit(element)">{{
element.birthday | date: 'dd.MM.yyyy'
}}</mat-cell>
</ng-container>
<ng-container matColumnDef="Gender">
<mat-header-cell *matHeaderCellDef> Geschlecht </mat-header-cell>
<mat-cell *matCellDef="let element" (click)="edit(element)">{{element | gender}}</mat-cell>
<mat-cell *matCellDef="let element" (click)="edit(element)">{{
element | gender
}}</mat-cell>
</ng-container>
<ng-container matColumnDef="Address">
<mat-header-cell *matHeaderCellDef> Adresse </mat-header-cell>
<mat-cell *matCellDef="let element" (click)="edit(element)">{{element | address}}</mat-cell>
<mat-cell *matCellDef="let element" (click)="edit(element)">{{
element | address
}}</mat-cell>
</ng-container>
<ng-container matColumnDef="Phone">
<mat-header-cell *matHeaderCellDef> Telefon </mat-header-cell>
<mat-cell *matCellDef="let element" (click)="edit(element)">{{element.phone}}</mat-cell>
<mat-cell *matCellDef="let element" (click)="edit(element)">{{
element.phone
}}</mat-cell>
</ng-container>
<ng-container matColumnDef="E-Mail">
<mat-header-cell *matHeaderCellDef> E-Mail </mat-header-cell>
<mat-cell *matCellDef="let element" (click)="edit(element)">{{element.email}}</mat-cell>
<mat-cell *matCellDef="let element" (click)="edit(element)">{{
element.email
}}</mat-cell>
</ng-container>
<ng-container matColumnDef="Actions">
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let element" class="actions"><a (click)="delete(element)"><i class="bi bi-trash"></i></a></mat-cell>
<mat-cell *matCellDef="let element" class="actions"
><a (click)="delete(element)"><i class="bi bi-trash"></i></a
></mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="[ 'Name', 'Enroll', 'Birthday', 'Gender', 'Address', 'Phone', 'E-Mail', 'Actions' ] "></mat-header-row>
<mat-row *matRowDef="let row; columns: [ 'Name', 'Enroll', 'Birthday', 'Gender', 'Address', 'Phone', 'E-Mail', 'Actions' ] "></mat-row>
<mat-header-row
*matHeaderRowDef="[
'Name',
'Enroll',
'Birthday',
'Gender',
'Address',
'Phone',
'E-Mail',
'Actions'
]"></mat-header-row>
<mat-row
*matRowDef="
let row;
columns: [
'Name',
'Enroll',
'Birthday',
'Gender',
'Address',
'Phone',
'E-Mail',
'Actions'
]
"></mat-row>
</mat-table>
</ng-template>
</div>
<div class="grid-item">
<input matInput id="pipelineFilter" (keyup)="applyFilter($event)" placeholder="Filtern">
</div>
<div class="grid-item">
<input
matInput
id="pipelineFilter"
(keyup)="applyFilter($event)"
placeholder="Filtern" />
</div>
<div class="grid-item"></div>
</div>
<li-student-edit [student]="selectedStudent" (closing)="commit()"></li-student-edit>
<li-student-enroll [student]="enrollingStudent" (closing)="commit()"></li-student-enroll>
<li-student-edit
[student]="selectedStudent"
(closing)="commit()"></li-student-edit>
<li-student-enroll
[student]="enrollingStudent"
(closing)="commit()"></li-student-enroll>

View File

@ -50,20 +50,20 @@ a:hover {
.bi {
color: #411ccc;
font-size: 2em;
margin-right:0.4em;
margin-right: 0.4em;
cursor: pointer;
}
.mat-row:hover .mat-cell {
.mat-mdc-row:hover .mat-mdc-cell {
background-color: #411ccc;
}
.mat-row:hover .mat-cell.actions .bi {
.mat-mdc-row:hover .mat-mdc-cell.actions .bi {
color: white;
}
#pipelineFilter{
display:block;
#pipelineFilter {
display: block;
height: 2em;
margin-bottom: 1.5em;
font-size: 1em;

View File

@ -9,10 +9,9 @@ import { StudentsService } from 'src/app/services/students/students.service';
@Component({
selector: 'li-student-list',
templateUrl: './student-list.component.html',
styleUrls: ['./student-list.component.scss']
styleUrls: ['./student-list.component.scss'],
})
export class StudentListComponent implements OnInit {
public loading: boolean = true;
public students: Student[] = new Array<Student>();
public courses: Course[] = new Array<Course>();
@ -23,22 +22,24 @@ export class StudentListComponent implements OnInit {
public constructor(
private studentsService: StudentsService,
private coursesService: CoursesService) { }
private coursesService: CoursesService
) {}
public ngOnInit() {
this.dataSource.filterPredicate = function (record: any, filter: any) {
if(filter.length < 3) {
if (filter.length < 3) {
return true;
}
const searchIn = (record.firstname?.toLocaleLowerCase()
+ record.lastname?.toLocaleLowerCase()
+ record.street?.toLocaleLowerCase()
+ record.email?.toLocaleLowerCase()) || '';
const searchIn =
record.firstname?.toLocaleLowerCase() +
record.lastname?.toLocaleLowerCase() +
record.street?.toLocaleLowerCase() +
record.email?.toLocaleLowerCase() || '';
const searchFor = filter.toLocaleLowerCase() || '';
return searchIn.indexOf(searchFor) >= 0;
}
};
this.getData();
}
@ -63,13 +64,18 @@ export class StudentListComponent implements OnInit {
public enroll(student: Student): void {
const enrollingStudent = Object.assign({}, student);
enrollingStudent.enrollments = this.courses.map(c => <StudentEnrollment> {
enrollingStudent.enrollments = this.courses.map(
c =>
<StudentEnrollment>{
cid: c.cid,
name: c.name,
diffname: c.diffname,
begin: student?.enrollments.find(e => e.cid === c.cid)?.begin || undefined,
end: student?.enrollments.find(e => e.cid === c.cid)?.end || undefined,
});
begin:
student?.enrollments.find(e => e.cid === c.cid)?.begin || undefined,
end:
student?.enrollments.find(e => e.cid === c.cid)?.end || undefined,
}
);
this.enrollingStudent = enrollingStudent;
}
@ -81,16 +87,14 @@ export class StudentListComponent implements OnInit {
}
private getData() {
this.studentsService.get()
.subscribe({
this.studentsService.get().subscribe({
next: students => {
this.loading = false;
this.students = students;
this.dataSource.data = this.students;
}
},
});
this.coursesService.get()
.subscribe(c => this.courses = c);
this.coursesService.get().subscribe(c => (this.courses = c));
}
}

View File

@ -1,4 +1,4 @@
<div>
<mat-calendar [(selected)]="selected"></mat-calendar>
<p>Selected date: {{selected}}</p>
<p>Selected date: {{ selected }}</p>
</div>

View File

@ -8,9 +8,8 @@ describe('VisitsDatetimeComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ VisitsDatetimeComponent ]
})
.compileComponents();
declarations: [VisitsDatetimeComponent],
}).compileComponents();
});
beforeEach(() => {

View File

@ -3,15 +3,12 @@ import { Component, OnInit } from '@angular/core';
@Component({
selector: 'li-visits-datetime',
templateUrl: './visits-datetime.component.html',
styleUrls: ['./visits-datetime.component.scss']
styleUrls: ['./visits-datetime.component.scss'],
})
export class VisitsDatetimeComponent implements OnInit {
selected: Date = new Date();
constructor() { }
ngOnInit(): void {
}
constructor() {}
ngOnInit(): void {}
}

View File

@ -4,37 +4,45 @@
<div *ngIf="courseVisit" id="content">
<div class="grid-item">
<h1>{{courseVisit?.name}}</h1>
<h2>{{courseVisit?.date | date: 'dd.MM.yyyy'}}, {{courseVisit?.begin}} - {{courseVisit?.end}}</h2>
<h1>{{ courseVisit?.name }}</h1>
<h2>
{{ courseVisit?.date | date: 'dd.MM.yyyy' }}, {{ courseVisit?.begin }} -
{{ courseVisit?.end }}
</h2>
<button class="button-add" (click)="add()">Hinzufügen</button>
</div>
<div class="grid-item">
</div>
<div class="grid-item"></div>
<div class="grid-item-full">
<div *ngIf="loading;else table">
<div *ngIf="loading; else table">
<mat-spinner class="center"></mat-spinner>
</div>
<ng-template #table>
<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="Name">
<th mat-header-cell *matHeaderCellDef> Name </th>
<td mat-cell *matCellDef="let element">{{element | name}}</td>
<th mat-header-cell *matHeaderCellDef>Name</th>
<td mat-cell *matCellDef="let element">{{ element | name }}</td>
</ng-container>
<ng-container matColumnDef="Visited">
<th mat-header-cell *matHeaderCellDef> Anwesend </th>
<td mat-cell *matCellDef="let element"><input type="checkbox" [checked]="element.visited > 0" (change)="visit(element)"/></ng-container>
<th mat-header-cell *matHeaderCellDef>Anwesend</th>
<td mat-cell *matCellDef="let element">
<input
type="checkbox"
[checked]="element.visited > 0"
(change)="visit(element)" /></td
></ng-container>
<tr mat-header-row *matHeaderRowDef="[ 'Name', 'Visited' ] "></tr>
<tr mat-row *matRowDef="let row; columns: [ 'Name', 'Visited' ] "></tr>
<tr mat-header-row *matHeaderRowDef="['Name', 'Visited']"></tr>
<tr mat-row *matRowDef="let row; columns: ['Name', 'Visited']"></tr>
</table>
</ng-template>
</div>
<div class="grid-item">
</div>
<div class="grid-item">
</div>
<div class="grid-item"></div>
<div class="grid-item"></div>
</div>
<li-student-edit [student]="addedStudent" (closing)="commit()" [simple]="true"></li-student-edit>
<li-student-edit
[student]="addedStudent"
(closing)="commit()"
[simple]="true"></li-student-edit>

View File

@ -42,9 +42,9 @@ a:hover {
color: aqua;
}
input[type=checkbox] {
width:2em;
height:2em;
input[type='checkbox'] {
width: 2em;
height: 2em;
}
.grid-item-full {

View File

@ -14,10 +14,9 @@ import { VisitsService } from 'src/app/services/visits/visits.service';
@Component({
selector: 'li-visits',
templateUrl: './visits.component.html',
styleUrls: ['./visits.component.scss']
styleUrls: ['./visits.component.scss'],
})
export class VisitsComponent implements OnInit, OnDestroy {
public loading: boolean = true;
public courseVisit?: CourseVisit;
public dataSource = new MatTableDataSource<StudentVisit>();
@ -30,7 +29,8 @@ export class VisitsComponent implements OnInit, OnDestroy {
public constructor(
private visitsService: VisitsService,
private enrollService: EnrollService,
private route: ActivatedRoute) { }
private route: ActivatedRoute
) {}
public ngOnInit() {
this.routerSubscription = this.route.params.subscribe(params => {
@ -56,13 +56,13 @@ export class VisitsComponent implements OnInit, OnDestroy {
}
public visit(studentVisit: StudentVisit): void {
const visit = <Visit> {
const visit = <Visit>{
cid: this.courseVisit?.cid,
sid: studentVisit.sid,
date: this.courseVisit?.date
}
date: this.courseVisit?.date,
};
if(studentVisit.visited > 0) {
if (studentVisit.visited > 0) {
this.visitsService.del(visit).subscribe(_ => this.getData());
} else {
this.visitsService.set(visit).subscribe(_ => this.getData());
@ -70,15 +70,14 @@ export class VisitsComponent implements OnInit, OnDestroy {
}
private getData(): void {
this.visitsService.get(this.courseDate)
.subscribe({
this.visitsService.get(this.courseDate).subscribe({
next: courseVisit => {
this.loading = false;
this.courseVisit = courseVisit;
if(this.courseVisit) {
if (this.courseVisit) {
this.dataSource.data = this.courseVisit.students;
}
}
},
});
}
@ -87,13 +86,15 @@ export class VisitsComponent implements OnInit, OnDestroy {
}
public commit(): void {
this.enrollService.set(
-1,
[<StudentEnrollment>{
this.enrollService
.set(-1, [
<StudentEnrollment>{
cid: this.courseVisit?.cid,
begin: formatDate(new Date(), 'yyyy-MM-dd', 'en-US'),
end: formatDate(new Date('2100-01-01'), 'yyyy-MM-dd', 'en-US')
}]).subscribe(_ => {
end: formatDate(new Date('2100-01-01'), 'yyyy-MM-dd', 'en-US'),
},
])
.subscribe(_ => {
this.addedStudent = undefined;
this.getData();
});

View File

@ -1,4 +1,4 @@
import { StudentVisit } from "./student-visit";
import { StudentVisit } from './student-visit';
export interface CourseVisit {
cid: number;

View File

@ -1,4 +1,4 @@
import { StudentEnrollment } from "./student-enrollment";
import { StudentEnrollment } from './student-enrollment';
export interface Student {
sid: number;

View File

@ -1,10 +1,10 @@
import { Pipe, PipeTransform } from '@angular/core';
import { Student } from '../models/student';
@Pipe({name: 'address'})
@Pipe({ name: 'address' })
export class AddressPipe implements PipeTransform {
transform(student: Student): string {
let result = `${student.street} ${student.house}${student.house_suffix}, ${student.zip} ${student.city}`
let result = `${student.street} ${student.house}${student.house_suffix}, ${student.zip} ${student.city}`;
return result.trim() === '0,' ? '' : result;
}
}

View File

@ -2,12 +2,12 @@ import { Pipe, PipeTransform } from '@angular/core';
import { Student } from '../models/student';
@Pipe({
name: 'enroll'
name: 'enroll',
})
export class EnrollPipe implements PipeTransform {
transform(student: Student): string {
return student.enrollments?.length > 0 ? student.enrollments?.map(e => e.name).join(', ') : '+';
return student.enrollments?.length > 0
? student.enrollments?.map(e => e.name).join(', ')
: '+';
}
}

View File

@ -1,13 +1,16 @@
import { Pipe, PipeTransform } from '@angular/core';
import { Student } from '../models/student';
@Pipe({name: 'gender'})
@Pipe({ name: 'gender' })
export class GenderPipe implements PipeTransform {
transform(student: Student): string {
switch(Number(student.gender)) {
case 0: return 'M';
case 1: return 'W';
default: return '?';
switch (Number(student.gender)) {
case 0:
return 'M';
case 1:
return 'W';
default:
return '?';
}
}
}

View File

@ -2,7 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core';
import { Student } from '../models/student';
import { StudentVisit } from '../models/student-visit';
@Pipe({name: 'name'})
@Pipe({ name: 'name' })
export class NamePipe implements PipeTransform {
transform(student: Student | StudentVisit): string {
return `${student.firstname} ${student.lastname}`;

View File

@ -5,14 +5,16 @@ import { Course } from 'src/app/models/course';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class CoursesService {
private readonly serviceName = "courses";
private readonly serviceName = 'courses';
constructor(private http: HttpClient) { }
constructor(private http: HttpClient) {}
public get(): Observable<Course[]> {
return this.http.get<Course[]>(`${environment.apiUrl}${this.serviceName}/get.php`);
return this.http.get<Course[]>(
`${environment.apiUrl}${this.serviceName}/get.php`
);
}
}

View File

@ -7,21 +7,32 @@ import { StudentEnrollment } from 'src/app/models/student-enrollment';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class EnrollService {
private readonly serviceName = "enroll";
private readonly serviceName = 'enroll';
constructor(private http: HttpClient) { }
constructor(private http: HttpClient) {}
public get(cid: number, date: Date): Observable<Enrollment> {
let params = new HttpParams().set('cid', cid).set('date', formatDate(date, 'yyyy-MM-dd', '' ));
return this.http.get<Enrollment>(`${environment.apiUrl}${this.serviceName}/get.php`, { params: params });
let params = new HttpParams()
.set('cid', cid)
.set('date', formatDate(date, 'yyyy-MM-dd', ''));
return this.http.get<Enrollment>(
`${environment.apiUrl}${this.serviceName}/get.php`,
{ params: params }
);
}
public set(sid: number, enrollments: StudentEnrollment[]): Observable<boolean> {
public set(
sid: number,
enrollments: StudentEnrollment[]
): Observable<boolean> {
const payload = `sid=${sid}&enrollments=${JSON.stringify(enrollments)}`;
return this.http.post<boolean>(`${environment.apiUrl}${this.serviceName}/set.php`, payload);
return this.http.post<boolean>(
`${environment.apiUrl}${this.serviceName}/set.php`,
payload
);
}
}

View File

@ -5,15 +5,17 @@ import { Student } from 'src/app/models/student';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class StudentsService {
private readonly serviceName = "students";
private readonly serviceName = 'students';
constructor(private http: HttpClient) { }
constructor(private http: HttpClient) {}
public get(): Observable<Student[]> {
return this.http.get<Student[]>(`${environment.apiUrl}${this.serviceName}/get.php`);
return this.http.get<Student[]>(
`${environment.apiUrl}${this.serviceName}/get.php`
);
}
public set(student: Student): Observable<void> {
@ -22,12 +24,18 @@ export class StudentsService {
&house=${student.house}&house_suffix=${student.house_suffix}&zip=${student.zip}
&city=${student.city}&phone=${student.phone}&email=${student.email}`;
return this.http.post<void>(`${environment.apiUrl}${this.serviceName}/set.php`, payload);
return this.http.post<void>(
`${environment.apiUrl}${this.serviceName}/set.php`,
payload
);
}
public del(student: Student): Observable<void> {
const payload = `sid=${student.sid}`;
return this.http.post<void>(`${environment.apiUrl}${this.serviceName}/del.php`, payload);
return this.http.post<void>(
`${environment.apiUrl}${this.serviceName}/del.php`,
payload
);
}
}

View File

@ -7,30 +7,39 @@ import { Visit } from 'src/app/models/visit';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class VisitsService {
private readonly serviceName = "visits";
private readonly serviceName = 'visits';
constructor(private http: HttpClient) { }
constructor(private http: HttpClient) {}
public get(date: Date): Observable<CourseVisit> {
const payload = `date=${formatDate(date, 'yyyy-MM-dd', 'en-US' )}&time=${formatDate(date, 'HH:mm', 'en-US' )}`;
const payload = `date=${formatDate(date, 'yyyy-MM-dd', 'en-US')}&time=${formatDate(date, 'HH:mm', 'en-US')}`;
// Not easy to pass "time" over GET
return this.http.post<CourseVisit>(`${environment.apiUrl}${this.serviceName}/get.php`, payload);
return this.http.post<CourseVisit>(
`${environment.apiUrl}${this.serviceName}/get.php`,
payload
);
}
public set(visit: Visit): Observable<boolean> {
const payload = `cid=${visit.cid}&sid=${visit.sid}&date=${formatDate(visit.date, 'yyyy-MM-dd', 'en-US' )}`
const payload = `cid=${visit.cid}&sid=${visit.sid}&date=${formatDate(visit.date, 'yyyy-MM-dd', 'en-US')}`;
return this.http.post<boolean>(`${environment.apiUrl}${this.serviceName}/set.php`, payload);
return this.http.post<boolean>(
`${environment.apiUrl}${this.serviceName}/set.php`,
payload
);
}
public del(visit: Visit): Observable<boolean> {
// Delete accepts no payload
const payload = `cid=${visit.cid}&sid=${visit.sid}&date=${formatDate(visit.date, 'yyyy-MM-dd', 'en-US' )}`
const payload = `cid=${visit.cid}&sid=${visit.sid}&date=${formatDate(visit.date, 'yyyy-MM-dd', 'en-US')}`;
return this.http.post<boolean>(`${environment.apiUrl}${this.serviceName}/del.php`, payload);
return this.http.post<boolean>(
`${environment.apiUrl}${this.serviceName}/del.php`,
payload
);
}
}

View File

@ -1,4 +1,4 @@
export const environment = {
production: true,
apiUrl: 'https://li-dance.de/plan/api/'
apiUrl: 'https://li-dance.de/plan/api/',
};

View File

@ -4,7 +4,7 @@
export const environment = {
production: false,
apiUrl: 'https://li-dance.de/plan/api/'
apiUrl: 'https://li-dance.de/plan/api/',
};
/*

View File

@ -1,16 +1,20 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<head>
<meta charset="utf-8" />
<title>Li-Dance Backoffice</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="mat-typography">
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap"
rel="stylesheet" />
<link
href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet" />
</head>
<body class="mat-typography">
<li-root></li-root>
</body>
</body>
</html>

View File

@ -8,5 +8,6 @@ if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));

View File

@ -47,7 +47,6 @@
*/
import 'zone.js'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

View File

@ -0,0 +1 @@
import 'jest-preset-angular/setup-jest';

View File

@ -1,6 +1,6 @@
/* Provide sufficient contrast against white background */
@import "~bootstrap-icons/font/bootstrap-icons.css";
@import '~bootstrap-icons/font/bootstrap-icons.css';
a {
color: #0366d6;
}
@ -23,7 +23,7 @@ body {
body {
background-color: rgb(245, 126, 32);
margin: 0;
font-family: Roboto, "Helvetica Neue", sans-serif;
font-family: Roboto, 'Helvetica Neue', sans-serif;
}
.mat-calendar-body-cell-content {
@ -38,7 +38,6 @@ body {
}
.mat-calendar-table-header {
tr {
th {
padding-top: 2em;

View File

@ -1,26 +0,0 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(path: string, deep?: boolean, filter?: RegExp): {
<T>(id: string): T;
keys(): string[];
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(),
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

@ -5,11 +5,6 @@
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.d.ts"
]
"files": ["src/main.ts", "src/polyfills.ts"],
"include": ["src/**/*.d.ts"]
}

View File

@ -16,12 +16,10 @@
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "es2017",
"target": "ES2022",
"module": "es2020",
"lib": [
"es2020",
"dom"
]
"lib": ["es2020", "dom"],
"useDefineForClassFields": false
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,

View File

@ -3,16 +3,8 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
"types": ["jest", "node"]
},
"files": [
"src/test.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
"files": ["src/test.ts", "src/polyfills.ts"],
"include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
}