Initial commit

This commit is contained in:
Artur Savitskiy 2022-11-30 15:26:46 +01:00
parent dfc1b8411c
commit 66ef196617
56 changed files with 21586 additions and 1 deletions

15
.browserslistrc Normal file
View File

@ -0,0 +1,15 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 iOS major versions
Firefox ESR

16
.editorconfig Normal file
View File

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

43
.gitignore vendored Normal file
View File

@ -0,0 +1,43 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
.android/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

4
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

20
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,20 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "pwa-chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}

42
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,42 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

View File

@ -1,2 +1,27 @@
# li-dance-backoffice # LiDanceBackoffice
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 13.1.3.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

111
angular.json Normal file
View File

@ -0,0 +1,111 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"li-dance-backoffice": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
},
"@schematics/angular:application": {
"strict": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "li",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/li-dance-backoffice",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "li-dance-backoffice:build:production"
},
"development": {
"browserTarget": "li-dance-backoffice:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "li-dance-backoffice:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"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": []
}
}
}
}
},
"defaultProject": "li-dance-backoffice"
}

44
karma.conf.js Normal file
View File

@ -0,0 +1,44 @@
// 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
});
};

19982
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

42
package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "li-dance-backoffice",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "~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",
"rxjs": "~7.4.0",
"tslib": "^2.3.0",
"zone.js": "~0.11.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",
"@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"
}
}

View File

@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { StudentListComponent } from './components/students/student-list/student-list.component';
import { VisitsComponent } from './components/visits/visits.component';
const routes: Routes = [
{ path: 'students', component: StudentListComponent },
{ path: 'visits', component: VisitsComponent },
{ path: '**', redirectTo: 'students' }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

@ -0,0 +1,5 @@
<body>
<div class="container">
<router-outlet></router-outlet>
</div>
</body>

View File

View File

@ -0,0 +1,35 @@
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'li-dance-backoffice'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('li-dance-backoffice');
});
it('should render title', () => {
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!');
});
});

10
src/app/app.component.ts Normal file
View File

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

45
src/app/app.module.ts Normal file
View File

@ -0,0 +1,45 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { StudentListComponent } from './components/students/student-list/student-list.component';
import { MatTableModule } from '@angular/material/table';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { HttpClientModule } from '@angular/common/http';
import { GenderPipe } from './pipes/gender.pipe';
import { AddressPipe } from './pipes/address.pipe';
import { NamePipe } from './pipes/name.pipe';
import { EnrollPipe } from './pipes/enroll.pipe';
import { VisitsComponent } from './components/visits/visits.component';
import { StudentEditComponent } from './components/students/student-edit/student-edit.component';
import { StudentEnrollComponent } from './components/students/student-enroll/student-enroll.component';
@NgModule({
declarations: [
AppComponent,
StudentListComponent,
StudentEditComponent,
GenderPipe,
AddressPipe,
NamePipe,
EnrollPipe,
VisitsComponent,
StudentEnrollComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
FormsModule,
AppRoutingModule,
HttpClientModule,
MatTableModule,
MatPaginatorModule,
MatProgressSpinnerModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

View File

@ -0,0 +1,44 @@
<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-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-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>
</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-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-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-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" />
</form>
<div style="text-align:center;">
<button class="button-save" (click)="save()">Save</button>
<button class="button-close" (click)="close()">Close</button>
</div>
</div>
</div>

View File

@ -0,0 +1,75 @@
.hidden{
display:none;
}
.show{
display:grid;
}
#student-edit-container {
grid-column: 2 / 3;
background: rgb(245, 126, 32);
color: #ffffff;
box-shadow: 0px 0px 2px 1px black;
z-index: 100;
padding: 2em;
overflow-y: scroll;
}
#student-edit-overlay {
position: fixed;
inset: 0;
z-index: 99;
background: rgba(0,0,0,0.8);
grid-template-columns: 1fr 1fr 1fr;
}
@media (min-width: 51em) and (max-width: 100em) {
#student-edit-overlay {
grid-template-columns: 1fr 3fr 1fr;
}
#student-edit-container {
grid-column: 2 / 3;
}
}
@media (max-width: 50em) {
#student-edit-overlay {
grid-template-columns: 1fr 3fr 1fr;
}
#student-edit-container {
grid-column: 1 / 4;
}
}
button {
color: white;
font-size: 1.5em;
}
button.button-save {
background-color: #411ccc;
}
button.button-close {
margin-left: 1em;
background-color: red;
}
label {
display:block;
width:100%;
margin-bottom: 0.5em;
}
select,
input[type=date],
input[type=text]{
display:block;
width:100%;
height: 2em;
margin-bottom: 1.5em;
font-size: 1em;
}

View File

@ -0,0 +1,49 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Student } from 'src/app/models/student';
import { StudentsService } from 'src/app/services/students/students.service';
@Component({
selector: 'li-student-edit',
templateUrl: './student-edit.component.html',
styleUrls: ['./student-edit.component.scss']
})
export class StudentEditComponent implements OnInit {
@Input()
public student?: Student = undefined;
@Output()
public closing = new EventEmitter();
public genders = [
{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) { }
ngOnInit(): void {
}
public close(): void {
this.closing.emit();
}
public save(): void {
if(this.student){
this.studentsService.set(this.model).subscribe(_ => this.closing.emit());
} else {
this.closing.emit();
}
}
public visibility(): string {
return this.student ? 'show' : 'hidden';
}
}

View File

@ -0,0 +1,28 @@
<div id="student-enroll-overlay" [class]="visibility()">
<div id="student-enroll-container">
<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 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>
</div>
<div>
<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>
</div>
</ng-container>
</div>
</form>
<div style="text-align:center;">
<button class="button-save" (click)="save()">Save</button>
<button class="button-close" (click)="close()">Close</button>
</div>
</div>
</div>

View File

@ -0,0 +1,65 @@
.hidden{
display:none;
}
.show{
display:grid;
}
#student-enroll-container {
grid-column: 2 / 3;
background: rgb(245, 126, 32);
color: #ffffff;
box-shadow: 0px 0px 2px 1px black;
z-index: 100;
padding: 2em;
overflow-y: scroll;
}
#student-enroll-overlay {
position: fixed;
inset: 0;
z-index: 99;
background: rgba(0,0,0,0.8);
grid-template-columns: 1fr 1fr 1fr;
}
@media (min-width: 51em) and (max-width: 100em) {
#student-enroll-overlay {
grid-template-columns: 1fr 3fr 1fr;
}
#student-enroll-container {
grid-column: 2 / 3;
}
}
@media (max-width: 50em) {
#student-enroll-overlay {
grid-template-columns: 1fr 3fr 1fr;
}
#student-enroll-container {
grid-column: 1 / 4;
}
}
.enrollment-name {
line-height: 3em;
}
button {
color: white;
font-size: 1.5em;
}
button.button-add,
button.button-save {
background-color: #411ccc;
}
button.button-remove,
button.button-close {
margin-left: 1em;
background-color: red;
}

View File

@ -0,0 +1,59 @@
import { formatDate } from '@angular/common';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Student } from 'src/app/models/student';
import { StudentEnrollment } from 'src/app/models/student-enrollment';
import { EnrollService } from 'src/app/services/enroll/enroll.service';
@Component({
selector: 'li-student-enroll',
templateUrl: './student-enroll.component.html',
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 ngOnInit() {
}
public enroll(enrollment: StudentEnrollment) {
enrollment.begin = formatDate(new Date(), 'yyyy-MM-dd', 'en-US');
enrollment.end = formatDate(new Date('2100-01-01'), 'yyyy-MM-dd', 'en-US');
}
public deroll(enrollment: StudentEnrollment) {
enrollment.begin = '';
enrollment.end = '';
}
public close(): void {
this.closing.emit();
}
public save(): void {
if (!this.student) {
this.closing.emit();
return;
}
this.enrollService.set(
this.student.sid,
this.student.enrollments.filter(e => e.begin))
.subscribe(_ => this.closing.emit());
}
public visibility(): string {
return this.student ? 'show' : 'hidden';
}
}

View File

@ -0,0 +1,67 @@
<div id="content">
<div class="grid-item">
<button class="button-add" (click)="add()">Hinzufügen</button>
</div>
<div class="grid-item">
</div>
<div class="grid-item-full">
<div *ngIf="loading;else table">
<mat-spinner class="center"></mat-spinner>
</div>
<ng-template #table>
<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>
</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>
</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>
</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>
</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>
</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>
</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>
</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>
</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-table>
</ng-template>
</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>

View File

@ -0,0 +1,74 @@
#content {
margin-top: 24px;
background-color: rgb(245, 126, 32);
color: #ffffff;
display: grid;
grid-template-columns: auto auto;
padding: 12px;
}
#pipelineFilter {
margin: 12px;
}
table {
width: 100%;
}
button {
color: white;
font-size: 1.5em;
}
button.button-add {
background-color: #411ccc;
}
.center {
width: 50%;
margin: auto;
}
a:link,
a:visited,
a:active {
text-decoration: none;
text-transform: none;
color: white;
}
a:hover {
color: #411ccc;
}
.grid-item-full {
grid-column-start: 1;
grid-column-end: 3;
}
.bi {
color: #411ccc;
font-size: 2em;
margin-right:0.4em;
cursor: pointer;
}
.mat-row:hover .mat-cell {
background-color: #411ccc;
}
.mat-row:hover .mat-cell.actions .bi {
color: white;
}
#pipelineFilter{
display:block;
height: 2em;
margin-bottom: 1.5em;
font-size: 1em;
}
.actions a {
margin-left: 3em;
}

View File

@ -0,0 +1,96 @@
import { Component, OnInit } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { Course } from 'src/app/models/course';
import { Student } from 'src/app/models/student';
import { StudentEnrollment } from 'src/app/models/student-enrollment';
import { CoursesService } from 'src/app/services/courses/courses.service';
import { StudentsService } from 'src/app/services/students/students.service';
@Component({
selector: 'li-student-list',
templateUrl: './student-list.component.html',
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>();
public selectedStudent?: Student;
public enrollingStudent?: Student;
public enrollments: StudentEnrollment[] = new Array<StudentEnrollment>();
public dataSource = new MatTableDataSource<Student>();
public constructor(
private studentsService: StudentsService,
private coursesService: CoursesService) { }
public ngOnInit() {
this.dataSource.filterPredicate = function (record: any, filter: any) {
if(filter.length < 3) {
return true;
}
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();
}
public applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value;
this.dataSource.filter = filterValue.trim().toLowerCase();
}
public add(): void {
this.selectedStudent = <Student>{};
}
public delete(student: Student): void {
this.studentsService.del(student).subscribe(_ => this.getData());
}
public edit(student: Student): void {
this.selectedStudent = student;
}
public enroll(student: Student): void {
const enrollingStudent = Object.assign({}, student);
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,
});
this.enrollingStudent = enrollingStudent;
}
public commit(): void {
this.selectedStudent = undefined;
this.enrollingStudent = undefined;
this.getData();
}
private getData() {
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);
}
}

View File

@ -0,0 +1,37 @@
<div *ngIf="!courseVisit" id="content">
<h1>Zur Zeit kein Kurs!</h1>
</div>
<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>
</div>
<div class="grid-item">
</div>
<div class="grid-item-full">
<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>
</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>
<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>

View File

@ -0,0 +1,44 @@
#content {
margin-top: 24px;
background-color: rgb(245, 126, 32);
color: #ffffff;
display: grid;
grid-template-columns: auto auto;
padding: 12px;
}
#pipelineFilter {
margin: 12px;
}
table {
width: 100%;
}
.center {
width: 50%;
margin: auto;
}
a:link,
a:visited,
a:active {
text-decoration: none;
text-transform: none;
color: white;
}
a:hover {
color: aqua;
}
input[type=checkbox] {
width:2em;
height:2em;
}
.grid-item-full {
grid-column-start: 1;
grid-column-end: 3;
}

View File

@ -0,0 +1,55 @@
import { Component, OnInit } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { CourseVisit } from 'src/app/models/course-visit';
import { StudentVisit } from 'src/app/models/student-visit';
import { Visit } from 'src/app/models/visit';
import { VisitsService } from 'src/app/services/visits/visits.service';
@Component({
selector: 'li-visits',
templateUrl: './visits.component.html',
styleUrls: ['./visits.component.scss']
})
export class VisitsComponent implements OnInit {
public loading: boolean = true;
public courseVisit?: CourseVisit;
public dataSource = new MatTableDataSource<StudentVisit>();
public constructor(private visitsService: VisitsService) { }
public ngOnInit() {
this.getData();
}
public visit(studentVisit: StudentVisit): void {
const visit = <Visit> {
cid: this.courseVisit?.cid,
sid: studentVisit.sid,
date: this.courseVisit?.date
}
if(studentVisit.visited > 0) {
this.visitsService.del(visit).subscribe(_ => this.getData());
} else {
this.visitsService.set(visit).subscribe(_ => this.getData());
}
}
private getData(): void {
//const now = new Date('2022-11-03 19:17:00');
const now = new Date();
this.visitsService.get(now)
.subscribe({
next: courseVisit => {
this.loading = false;
this.courseVisit = courseVisit;
if(this.courseVisit) {
this.dataSource.data = this.courseVisit.students;
}
}
});
}
}

View File

@ -0,0 +1,10 @@
import { StudentVisit } from "./student-visit";
export interface CourseVisit {
cid: number;
name: string;
date: Date;
begin: string;
end: string;
students: StudentVisit[];
}

5
src/app/models/course.ts Normal file
View File

@ -0,0 +1,5 @@
export interface Course {
cid: number;
name: string;
diffname: string;
}

View File

@ -0,0 +1,6 @@
export interface Enrollment {
sid: number;
cid: number;
begin: Date;
end: Date;
}

View File

@ -0,0 +1,7 @@
export interface StudentEnrollment {
cid: number;
name: string;
diffname: string;
begin: string;
end: string;
}

View File

@ -0,0 +1,6 @@
export interface StudentVisit {
sid: number;
firstname: string;
lastname: string;
visited: number;
}

17
src/app/models/student.ts Normal file
View File

@ -0,0 +1,17 @@
import { StudentEnrollment } from "./student-enrollment";
export interface Student {
sid: number;
firstname: string;
lastname: string;
birthday: Date;
gender: number;
street: string;
house: number;
house_suffix: string;
zip: string;
city: string;
phone: string;
email: string;
enrollments: StudentEnrollment[];
}

5
src/app/models/visit.ts Normal file
View File

@ -0,0 +1,5 @@
export interface Visit {
cid: number;
sid: number;
date: Date;
}

View File

@ -0,0 +1,10 @@
import { Pipe, PipeTransform } from '@angular/core';
import { Student } from '../models/student';
@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}`
return result.trim() === '0,' ? '' : result;
}
}

View File

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

View File

@ -0,0 +1,13 @@
import { Pipe, PipeTransform } from '@angular/core';
import { Student } from '../models/student';
@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 '?';
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,28 @@
import { formatDate } from '@angular/common';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Enrollment } from 'src/app/models/enrollment';
import { Student } from 'src/app/models/student';
import { StudentEnrollment } from 'src/app/models/student-enrollment';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class EnrollService {
private readonly serviceName = "enroll";
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 });
}
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);
}
}

View File

@ -0,0 +1,33 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Student } from 'src/app/models/student';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class StudentsService {
private readonly serviceName = "students";
constructor(private http: HttpClient) { }
public get(): Observable<Student[]> {
return this.http.get<Student[]>(`${environment.apiUrl}${this.serviceName}/get.php`);
}
public set(student: Student): Observable<void> {
const payload = `sid=${student.sid}&firstname=${student.firstname}&lastname=${student.lastname}
&birthday=${student.birthday}&gender=${student.gender}&street=${student.street}
&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);
}
public del(student: Student): Observable<void> {
const payload = `sid=${student.sid}`;
return this.http.post<void>(`${environment.apiUrl}${this.serviceName}/del.php`, payload);
}
}

View File

@ -0,0 +1,36 @@
import { DatePipe, formatDate } from '@angular/common';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { CourseVisit } from 'src/app/models/course-visit';
import { Visit } from 'src/app/models/visit';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class VisitsService {
private readonly serviceName = "visits";
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' )}`;
// Not easy to pass "time" over GET
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' )}`
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' )}`
return this.http.post<boolean>(`${environment.apiUrl}${this.serviceName}/del.php`, payload);
}
}

0
src/assets/.gitkeep Normal file
View File

View File

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

View File

@ -0,0 +1,17 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false,
apiUrl: 'https://li-dance.de/plan/api/'
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.

BIN
src/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

16
src/index.html Normal file
View File

@ -0,0 +1,16 @@
<!doctype html>
<html lang="de">
<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">
<li-root></li-root>
</body>
</html>

12
src/main.ts Normal file
View File

@ -0,0 +1,12 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

53
src/polyfills.ts Normal file
View File

@ -0,0 +1,53 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes recent versions of Safari, Chrome (including
* Opera), Edge on the desktop, and iOS and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

27
src/styles.scss Normal file
View File

@ -0,0 +1,27 @@
/* Provide sufficient contrast against white background */
@import "~bootstrap-icons/font/bootstrap-icons.css";
a {
color: #0366d6;
}
code {
color: #e01a76;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
html,
body {
height: 100%;
}
body {
background-color: rgb(245, 126, 32);
margin: 0;
font-family: Roboto, "Helvetica Neue", sans-serif;
}

26
src/test.ts Normal file
View File

@ -0,0 +1,26 @@
// 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);

15
tsconfig.app.json Normal file
View File

@ -0,0 +1,15 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.d.ts"
]
}

32
tsconfig.json Normal file
View File

@ -0,0 +1,32 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "es2017",
"module": "es2020",
"lib": [
"es2020",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

18
tsconfig.spec.json Normal file
View File

@ -0,0 +1,18 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"files": [
"src/test.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}