22 Commits

Author SHA1 Message Date
46f351ec5b Merge pull request #21 from Slasherss1/1.1.1
v1.1.1
2025-06-06 10:08:21 +02:00
06c88cd920 chore: version numbers 2025-06-06 10:07:18 +02:00
00e79e5f4d feat: Added a link to documentation in admin panel 2025-06-04 19:26:26 +02:00
c525dfe1c1 fix: Fixed test units 2025-06-04 16:24:18 +02:00
a14d860022 Update README.md 2025-06-03 14:35:59 +02:00
9f97e584bd Merge pull request #20 from Slasherss1/1.1.0
v1.1.0
2025-06-03 13:31:05 +02:00
00daf7c972 chore: Bumped version numbers 2025-06-03 13:26:28 +02:00
d4c7084820 feat: Added unchecked room highlighting. Resolves #11 2025-06-03 13:23:58 +02:00
0c60f39152 feat: Added menu items and account security to settings 2025-06-01 21:48:56 +02:00
ca6037d405 feat: added user search to various components 2025-06-01 17:44:48 +02:00
94702834b4 feat: Added user search component. Resolves #15 2025-06-01 13:54:47 +02:00
3b56d40d5a feat: Added admin start page 2025-06-01 10:25:05 +02:00
efd76e16a1 feat: Added notification dialog on frontend 2025-05-31 19:56:31 +02:00
375bb1ceb4 feat: Added notifications outbox to admin panel 2025-05-31 16:57:58 +02:00
86347e254b feat: Added redirect after login for users. Closes #17 2025-05-24 11:26:42 +02:00
cf2fa0b607 fix: Redesigned user cards 2025-05-21 19:56:25 +02:00
45fb44712e fix: Made menu empty if no items.
Not too elegant of a solution, but works.
Going to do the same in print display of backend. Probably not gonna be elegant aswell.
2025-05-20 22:02:51 +02:00
92768ceda6 fix: The date picker now outputs start of day 2025-05-20 21:22:49 +02:00
26dac21e7e fix: Added missing news message. Resolves #18. 2025-05-20 21:10:21 +02:00
7d98cc2c49 fix: Added serviceWorker env and fixed #19 2025-05-20 20:58:52 +02:00
90d5b5da1c feat: Made login errors download from server 2025-05-16 00:38:59 +02:00
6ab3598d38 v1.0.1 (#16)
* fix: Moved user type to a separate file. fix #7, fix #8

* fix: added more filters

* fix: Added attendence clear button and notes. fix #9, fix #10

* fix: bumped version number

* fix: Changed wording. Resolves #12

* fix: Resolve #13

* fix: Safari no longer displays system ui over lower guide.

* fix: Resolved #14
2025-05-13 19:28:21 +02:00
118 changed files with 2040 additions and 413 deletions

View File

@@ -1,8 +1,4 @@
# Ipwa # Ipwa
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.0.4. This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.0.4.
This project depends on the [Backend server](https://github.com/Slasherss1/ipwa-backend2) This project depends on the [Backend server](https://github.com/Slasherss1/ipwa-backend)
## Things to change
Change following files:
- (Optional) `src/assets/icons/*` - You can change the icons to your own

View File

@@ -70,6 +70,30 @@
"with": "src/environments/environment.development.ts" "with": "src/environments/environment.development.ts"
} }
] ]
},
"swDevelopment": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true,
"outputHashing": "all",
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.swdev.ts"
}
],
"assets": [
{
"glob": "ngsw-worker.js",
"input": "node_modules/@angular/servce-worker",
"output": "."
},
"src/ngsw.json",
"src/manifest.webmanifest"
]
} }
}, },
"defaultConfiguration": "production" "defaultConfiguration": "production"

View File

@@ -1,16 +1,16 @@
{ {
"$schema": "./node_modules/@angular/service-worker/config/schema.json", "$schema": "./node_modules/@angular/service-worker/config/schema.json",
"index": "./index.html", "index": "/ipwa/index.html",
"assetGroups": [ "assetGroups": [
{ {
"name": "app", "name": "app",
"installMode": "prefetch", "installMode": "prefetch",
"resources": { "resources": {
"files": [ "files": [
"/favicon.ico", "/ipwa/favicon.ico",
"/manifest.webmanifest", "/ipwa/manifest.webmanifest",
"/*.css", "/ipwa/*.css",
"/*.js" "/ipwa/*.js"
] ]
} }
}, },
@@ -20,8 +20,8 @@
"updateMode": "prefetch", "updateMode": "prefetch",
"resources": { "resources": {
"files": [ "files": [
"./assets/**", "/ipwa/assets/**",
"/**/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)" "/ipwa/**/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
] ]
} }
} }

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "ipwa", "name": "ipwa",
"version": "1.0.0", "version": "1.1.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "ipwa", "name": "ipwa",
"version": "1.0.0", "version": "1.1.1",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",

View File

@@ -3,7 +3,7 @@
<mat-label>Wyszukaj</mat-label> <mat-label>Wyszukaj</mat-label>
<input matInput (keyup)="filter($event)"> <input matInput (keyup)="filter($event)">
</mat-form-field> </mat-form-field>
<button mat-icon-button (click)="new()"><mat-icon>add</mat-icon></button> <button mat-icon-button (click)="openUserCard()"><mat-icon>add</mat-icon></button>
</div> </div>
<mat-spinner *ngIf="loading"></mat-spinner> <mat-spinner *ngIf="loading"></mat-spinner>
<table mat-table [dataSource]="users"> <table mat-table [dataSource]="users">
@@ -24,17 +24,9 @@
<td mat-cell *matCellDef="let element">{{element.uname}}</td> <td mat-cell *matCellDef="let element">{{element.uname}}</td>
</div> </div>
<div matColumnDef="actions"> <div matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Akcje</th> <th mat-header-cell *matHeaderCellDef>Karta użytkownika</th>
<td mat-cell *matCellDef="let element"> <td mat-cell *matCellDef="let element">
<button mat-mini-fab (click)="resetPass(element._id)"><mat-icon>lock_reset</mat-icon></button> <button mat-mini-fab (click)="openUserCard(element._id)"><mat-icon>manage_accounts</mat-icon></button>
<button mat-mini-fab (click)="edit(element)"><mat-icon>edit</mat-icon></button>
<button mat-mini-fab (click)="toggleLock(element)">
<div [ngSwitch]="element.locked">
<mat-icon *ngSwitchCase="true">lock</mat-icon>
<mat-icon *ngSwitchDefault>lock_open</mat-icon>
</div>
</button>
<button mat-mini-fab (click)="delete(element._id)"><mat-icon>delete_forever</mat-icon></button>
</td> </td>
</div> </div>
<tr mat-header-row *matHeaderRowDef="collumns"></tr> <tr mat-header-row *matHeaderRowDef="collumns"></tr>

View File

@@ -11,21 +11,35 @@ import { of } from 'rxjs';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
describe('AccountMgmtComponent', () => { describe('AccountMgmtComponent', () => {
let component: AccountMgmtComponent; let component: AccountMgmtComponent;
let fixture: ComponentFixture<AccountMgmtComponent>; let fixture: ComponentFixture<AccountMgmtComponent>;
let acMock
beforeEach(async () => { beforeEach(async () => {
const acMock = jasmine.createSpyObj("AdminCommService", { acMock = {
getAccs: of() accs: {
}) getAccs: jasmine.createSpy("getAccs").and.returnValue(of())
}
}
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [AccountMgmtComponent], declarations: [AccountMgmtComponent],
providers: [ providers: [
{provide: AdminCommService, useValue: acMock} {provide: AdminCommService, useValue: acMock}
], ],
imports: [MatDialogModule, MatSnackBarModule, MatFormFieldModule, MatIconModule, MatPaginatorModule, MatTableModule, MatInputModule, BrowserAnimationsModule] imports: [
MatDialogModule,
MatSnackBarModule,
MatFormFieldModule,
MatIconModule,
MatPaginatorModule,
MatTableModule,
MatInputModule,
BrowserAnimationsModule,
MatProgressSpinnerModule
]
}).compileComponents(); }).compileComponents();
fixture = TestBed.createComponent(AccountMgmtComponent); fixture = TestBed.createComponent(AccountMgmtComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@@ -4,12 +4,10 @@ import { MatDialog } from '@angular/material/dialog';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { UserDeleteComponent } from './user-delete/user-delete.component';
import { UserEditComponent } from './user-edit/user-edit.component'; import { UserEditComponent } from './user-edit/user-edit.component';
import { catchError, throwError } from 'rxjs';
import { UserResetComponent } from './user-reset/user-reset.component';
import { LocalStorageService } from 'src/app/services/local-storage.service'; import { LocalStorageService } from 'src/app/services/local-storage.service';
import { Group } from 'src/app/types/group'; import { Group } from 'src/app/types/group';
import User from 'src/app/types/user';
@Component({ @Component({
selector: 'app-account-mgmt', selector: 'app-account-mgmt',
@@ -20,21 +18,20 @@ import { Group } from 'src/app/types/group';
export class AccountMgmtComponent implements OnInit, AfterViewInit { export class AccountMgmtComponent implements OnInit, AfterViewInit {
protected groups: Group[] = [] protected groups: Group[] = []
users: MatTableDataSource<any> users: MatTableDataSource<Omit<User, "pass">>
loading = false loading = false
@ViewChild(MatPaginator) paginator!: MatPaginator @ViewChild(MatPaginator) paginator!: MatPaginator
constructor(readonly ac:AdminCommService, private dialog: MatDialog, private sb: MatSnackBar, protected readonly ls: LocalStorageService) { constructor(readonly ac:AdminCommService, private dialog: MatDialog, private sb: MatSnackBar, protected readonly ls: LocalStorageService) {
this.users = new MatTableDataSource<any>(); this.users = new MatTableDataSource<Omit<User, "pass">>();
this.users.filterPredicate = (data: Record<string, any>, filter: string): boolean => { this.users.filterPredicate = (data: Record<string, any>, filter: string): boolean => {
const dataStr = Object.keys(data).reduce((curr: string, key: string) => { const dataStr = Object.keys(data).reduce((curr: string, key: string) => {
if (key == "_id") { if (["_id", "admin", "groups", "__v", "locked"].find(v => v == key)) {
return '' return curr + ''
} }
return curr + data[key] + '⫂' return curr + data[key] + '⫂'
}, '').toLowerCase() }, '').toLowerCase()
const filternew = filter.trim().toLowerCase() const filternew = filter.trim().toLowerCase()
return dataStr.indexOf(filternew) != -1 return dataStr.indexOf(filternew) != -1
} }
} }
@@ -57,75 +54,9 @@ export class AccountMgmtComponent implements OnInit, AfterViewInit {
this.users.filter = value.toLowerCase().trim() this.users.filter = value.toLowerCase().trim()
} }
edit(item: any) { openUserCard(id?: string) {
this.dialog.open(UserEditComponent, {data: {user: item, groups: this.groups}}).afterClosed().subscribe(reply => { this.dialog.open<UserEditComponent, UserEditComponent.InputData, UserEditComponent.ReturnData>(UserEditComponent, {data: {id: id, type: id ? "edit" : "new", groups: this.groups}}).afterClosed().subscribe(r => {
if (reply) { if (r) this.ngOnInit()
this.ac.accs.putAcc(item._id, reply).pipe(catchError((err)=>{
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
return throwError(()=> new Error(err.message))
})).subscribe((data)=> {
if (data.status == 200) {
this.sb.open("Użytkownik został zmodyfikowany.", undefined, {duration: 2500})
this.ngOnInit()
} else {
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
}
})
}
})
}
new() {
this.dialog.open(UserEditComponent, {data: {groups: this.groups}}).afterClosed().subscribe(reply => {
if (reply) {
this.ac.accs.postAcc(reply).pipe(catchError((err)=>{
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
return throwError(()=> new Error(err.message))
})).subscribe((data)=> {
if (data.status == 201) {
this.sb.open("Użytkownik został utworzony.", undefined, {duration: 2500})
this.ngOnInit()
} else {
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
}
})
}
})
}
delete(id: string) {
this.dialog.open(UserDeleteComponent).afterClosed().subscribe(reply => {
if (reply) {
this.ac.accs.deleteAcc(id).subscribe((res) => {
if (res.status == 200) {
this.sb.open("Użytkownik został usunięty.", undefined, {duration: 2500})
this.ngOnInit()
} else {
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
console.error(res);
}
})
}
})
}
resetPass(id: string) {
this.dialog.open(UserResetComponent).afterClosed().subscribe((res) => {
if (res == true) {
this.ac.accs.resetPass(id).subscribe((patch)=>{
if (patch.status == 200) {
this.sb.open("Hasło zostało zresetowane", undefined, {duration: 2500})
}
})
}
})
}
toggleLock(item: any) {
this.ac.accs.putAcc(item._id, {locked: !item.locked}).subscribe((res) => {
if (res.status == 200) {
item.locked = !item.locked
}
}) })
} }

View File

@@ -1,39 +1,67 @@
<form [formGroup]="form" (ngSubmit)="editUser()"> <h1 mat-dialog-title>Karta użytkownika</h1>
<mat-form-field appearance="outline"> <mat-dialog-content>
<mat-label>Imię</mat-label> <form [formGroup]="form">
<input type="text" matInput formControlName="fname"> <div>
</mat-form-field> <mat-form-field appearance="outline" color="accent">
<mat-form-field appearance="outline"> <mat-label>Imię</mat-label>
<mat-label>Nazwisko</mat-label> <input type="text" matInput formControlName="fname">
<input type="text" matInput formControlName="surname"> </mat-form-field>
</mat-form-field> <mat-form-field appearance="outline" color="accent">
<mat-form-field appearance="outline"> <mat-label>Nazwisko</mat-label>
<mat-label>Pokój</mat-label> <input type="text" matInput formControlName="surname">
<input type="text" matInput formControlName="room"> </mat-form-field>
</mat-form-field> <mat-form-field appearance="outline" color="accent">
<mat-form-field appearance="outline"> <mat-label>Pokój</mat-label>
<mat-label>Nazwa użytkownika</mat-label> <input type="text" matInput formControlName="room">
<input type="text" matInput required formControlName="uname"> </mat-form-field>
</mat-form-field> <mat-form-field appearance="outline" color="accent">
<mat-form-field appearance="outline"> <mat-label>Grupy</mat-label>
<mat-label>Grupy</mat-label> <mat-select multiple formControlName="groups">
<mat-select multiple formControlName="groups"> @for (item of groups; track $index) {
@for (item of groups; track $index) { <mat-option [value]="item._id">{{item.name}}</mat-option>
<mat-option [value]="item._id">{{item.name}}</mat-option> }
</mat-select>
</mat-form-field>
<span *ngIf="data.type == 'edit'">Data rejestracji:<br>{{regDate?.format('DD.MM.YYYY')}}</span>
</div>
<div>
<mat-form-field appearance="outline" color="accent">
<mat-label>Nazwa użytkownika</mat-label>
<input type="text" matInput required formControlName="uname">
</mat-form-field>
@if (data.type == "edit") {
<button mat-stroked-button color="accent" (click)="resetPass()">Resetuj hasło</button>
@if (locked) {
<button mat-stroked-button color="warn" (click)="toggleLock(false)"><mat-icon>lock</mat-icon>Blokada ręczna</button>
} @else {
<button mat-stroked-button color="accent" (click)="toggleLock(true)">Zablokuj konto</button>
}
@if (lockout) {
<button mat-stroked-button color="warn" (click)="disableLockout()"><mat-icon>lock_clock</mat-icon>Auto-Blokada</button>
} @else {
<button mat-stroked-button disabled>Auto-Blokada nieczynna</button>
}
<mat-form-field *ngIf="ls.permChecker(32)" color="accent">
<mat-label>Uprawnienia</mat-label>
<mat-select multiple formControlName="flags">
<mat-option [value]="1" *ngIf="ls.capCheck(1)">Wiadomości</mat-option>
<mat-option [value]="2" *ngIf="ls.capCheck(2)">Jadłospis</mat-option>
<mat-option [value]="4" *ngIf="ls.capCheck(4)">Powiadomienia</mat-option>
<mat-option [value]="8" *ngIf="ls.capCheck(8)">Grupy</mat-option>
<mat-option [value]="16">Konta</mat-option>
<mat-option [value]="64" *ngIf="ls.capCheck(32)">Klucze</mat-option>
<mat-option [value]="128" *ngIf="ls.capCheck(16)">Czystość</mat-option>
</mat-select>
</mat-form-field>
} }
</mat-select> </div>
</mat-form-field> </form>
<mat-form-field *ngIf="this.ls.permChecker(32)"> </mat-dialog-content>
<mat-label>Uprawnienia</mat-label> <mat-dialog-actions>
<mat-select multiple formControlName="flags"> @if (data.type == "edit") {
<mat-option [value]="1" *ngIf="ls.capCheck(1)">Wiadomości</mat-option> <button mat-stroked-button color="warn" style="margin-right: auto;">Usuń konto</button>
<mat-option [value]="2" *ngIf="ls.capCheck(2)">Jadłospis</mat-option> }
<mat-option [value]="4" *ngIf="ls.capCheck(4)">Powiadomienia</mat-option> <button mat-stroked-button mat-dialog-close>Zamknij</button>
<mat-option [value]="8" *ngIf="ls.capCheck(8)">Grupy</mat-option> <button mat-flat-button color="accent" (click)="submit()">Zapisz</button>
<mat-option [value]="16">Konta</mat-option> <mat-spinner diameter="32" color="accent" *ngIf="loading"></mat-spinner>
<mat-option [value]="64" *ngIf="ls.capCheck(32)">Klucze</mat-option> </mat-dialog-actions>
<mat-option [value]="128" *ngIf="ls.capCheck(16)">Czystość</mat-option>
</mat-select>
</mat-form-field>
<button mat-stroked-button>Wyślij</button>
</form>

View File

@@ -4,7 +4,29 @@
} }
form { form {
margin-top: 1ch !important;
display: flex; display: flex;
flex-direction: column; grid-auto-flow: column;
flex-direction: row;
flex-wrap: wrap;
align-items: center; align-items: center;
justify-content: center;
column-gap: 3ch;
div {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: repeat(5, 1fr);
align-items: center;
button {
align-self: stretch;
justify-self: stretch;
height: auto;
margin-bottom: 1lh;
}
}
}
mat-dialog-actions {
display: flex;
justify-content: flex-end;
} }

View File

@@ -3,21 +3,36 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserEditComponent } from './user-edit.component'; import { UserEditComponent } from './user-edit.component';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { ReactiveFormsModule } from '@angular/forms'; import { NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { AdminCommService } from '../../admin-comm.service';
import { forwardRef } from '@angular/core';
import { MatSelectModule } from '@angular/material/select';
describe('UserEditComponent', () => { describe('UserEditComponent', () => {
let component: UserEditComponent; let component: UserEditComponent;
let fixture: ComponentFixture<UserEditComponent>; let fixture: ComponentFixture<UserEditComponent>;
let acMock
beforeEach(async () => { beforeEach(async () => {
acMock = {
}
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [UserEditComponent], declarations: [UserEditComponent],
imports: [MatDialogModule, MatFormFieldModule, ReactiveFormsModule, MatInputModule, BrowserAnimationsModule], imports: [
MatDialogModule,
MatFormFieldModule,
ReactiveFormsModule,
MatInputModule,
NoopAnimationsModule,
MatSelectModule
],
providers: [ providers: [
{provide: MatDialogRef, useValue: {}}, { provide: MatDialogRef, useValue: {} },
{provide: MAT_DIALOG_DATA, useValue: {}} { provide: MAT_DIALOG_DATA, useValue: { groups: [] } },
{ provide: AdminCommService, useValue: acMock },
] ]
}).compileComponents(); }).compileComponents();
fixture = TestBed.createComponent(UserEditComponent); fixture = TestBed.createComponent(UserEditComponent);

View File

@@ -1,52 +1,126 @@
import { Component, Inject } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { FormControl, FormGroup } from '@angular/forms'; import { FormControl, FormGroup } from '@angular/forms';
import { LocalStorageService } from 'src/app/services/local-storage.service'; import { LocalStorageService } from 'src/app/services/local-storage.service';
import { Group } from 'src/app/types/group'; import { Group } from 'src/app/types/group';
import { AdminCommService } from '../../admin-comm.service';
import { UserDeleteComponent } from '../user-delete/user-delete.component';
import { MatSnackBar } from '@angular/material/snack-bar';
import { UserResetComponent } from '../user-reset/user-reset.component';
import { catchError, throwError } from 'rxjs';
import { Moment } from 'moment';
import * as moment from 'moment';
export namespace UserEditComponent {
export type InputData = {type: "new" | "edit", id?: string, groups: Group[]}
export type ReturnData = true | undefined
}
@Component({ @Component({
selector: 'app-user-edit', selector: 'app-user-edit',
templateUrl: './user-edit.component.html', templateUrl: './user-edit.component.html',
styleUrls: ['./user-edit.component.scss'] styleUrls: ['./user-edit.component.scss']
}) })
export class UserEditComponent { export class UserEditComponent {
form: FormGroup lockout = false;
locked = false;
loading = false;
form: FormGroup = new FormGroup({
fname: new FormControl<string>(""),
surname: new FormControl<string>(""),
room: new FormControl<string>(""),
uname: new FormControl<string>(""),
groups: new FormControl<Array<string>>([]),
flags: new FormControl<Array<number>>([]),
})
groups: Group[] groups: Group[]
constructor (public dialogRef: MatDialogRef<UserEditComponent>, @Inject(MAT_DIALOG_DATA) public data: any, readonly ls: LocalStorageService) { id?: string
if (data.user == null) { regDate?: Moment;
data.user = { constructor (
fname: "", public dialogRef: MatDialogRef<UserEditComponent>,
surname: "", @Inject(MAT_DIALOG_DATA) public data: UserEditComponent.InputData,
room: "", readonly ls: LocalStorageService,
uname: "", readonly acu: AdminCommService,
groups: [], private dialog: MatDialog,
admin: 0 private sb: MatSnackBar
) {
this.groups = data.groups
if (data.type == "edit") {
this.id = data.id
this.acu.accs.getUser(data.id!).subscribe((r) => {
this.regDate = moment(r.regDate)
var flags: Array<number> = []
if (r.admin) {
if ((r.admin & 1) == 1) flags.push(1)
if ((r.admin & 2) == 2) flags.push(2)
if ((r.admin & 4) == 4) flags.push(4)
if ((r.admin & 8) == 8) flags.push(8)
if ((r.admin & 16) == 16) flags.push(16)
if ((r.admin & 32) == 32) flags.push(32)
if ((r.admin & 64) == 64) flags.push(64)
if ((r.admin & 128) == 128) flags.push(128)
}
this.locked = r.locked ? true : false
this.lockout = r.lockout
this.form.get("fname")?.setValue(r.fname)
this.form.get("surname")?.setValue(r.surname)
this.form.get("room")?.setValue(r.room)
this.form.get("uname")?.setValue(r.uname)
this.form.get("groups")?.setValue(r.groups)
this.form.get("flags")?.setValue(flags)
})
}
}
protected submit() {
this.loading = true
if (this.data.type == "edit") {
this.acu.accs.putAcc(this.id!, this.getForm()).pipe(catchError((err)=>{
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
return throwError(()=> new Error(err.message))
})).subscribe((data)=> {
if (data.status == 200) {
this.sb.open("Użytkownik został zmodyfikowany.", undefined, {duration: 2500})
this.dialogRef.close(true)
} else {
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
this.loading = false
}
})
} else {
this.acu.accs.postAcc(this.getForm()).pipe(catchError((err)=>{
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
return throwError(()=> new Error(err.message))
})).subscribe((data)=> {
if (data.status == 201) {
this.sb.open("Użytkownik został utworzony.", undefined, {duration: 2500})
this.dialogRef.close(true)
} else {
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
this.loading = false
}
})
}
}
protected disableLockout() {
this.loading = true
this.acu.accs.clearLockout(this.id!).pipe(catchError((err)=>{
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
return throwError(()=> new Error(err.message))
})).subscribe((s) => {
if (s.status == 200) {
this.loading = false
this.lockout = false
} else {
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
this.loading = false
} }
}
this.groups = data.groups ? data.groups : []
var flags: Array<number> = []
if (data.user.admin) {
if ((data.user.admin & 1) == 1) flags.push(1)
if ((data.user.admin & 2) == 2) flags.push(2)
if ((data.user.admin & 4) == 4) flags.push(4)
if ((data.user.admin & 8) == 8) flags.push(8)
if ((data.user.admin & 16) == 16) flags.push(16)
if ((data.user.admin & 32) == 32) flags.push(32)
if ((data.user.admin & 64) == 64) flags.push(64)
if ((data.user.admin & 128) == 128) flags.push(128)
}
this.form = new FormGroup({
fname: new FormControl(data.user.fname),
surname: new FormControl(data.user.surname),
room: new FormControl(data.user.room),
uname: new FormControl<string>(data.user.uname),
groups: new FormControl<Array<string>>(data.user.groups),
flags: new FormControl<Array<number>>(flags),
}) })
} }
protected editUser() { protected getForm() {
this.dialogRef.close({ return {
fname: this.form.get('fname')?.value, fname: this.form.get('fname')?.value,
surname: this.form.get('surname')?.value, surname: this.form.get('surname')?.value,
room: this.form.get('room')?.value, room: this.form.get('room')?.value,
@@ -60,6 +134,44 @@ export class UserEditComponent {
return undefined return undefined
} }
})() })()
}
}
protected delete() {
this.dialog.open(UserDeleteComponent).afterClosed().subscribe(reply => {
if (reply) {
this.acu.accs.deleteAcc(this.id!).subscribe((res) => {
if (res.status == 200) {
this.sb.open("Użytkownik został usunięty.", undefined, {duration: 2500})
this.dialogRef.close()
} else {
this.sb.open("Wystąpił błąd. Skontaktuj się z obsługą programu.")
console.error(res);
}
})
}
})
}
protected resetPass() {
this.loading = true
this.dialog.open(UserResetComponent).afterClosed().subscribe((res) => {
if (res == true) {
this.acu.accs.resetPass(this.id!).subscribe((patch)=>{
if (patch.status == 200) {
this.sb.open("Hasło zostało zresetowane", undefined, {duration: 2500})
this.loading = false
}
})
}
})
}
protected toggleLock(state: boolean) {
this.acu.accs.putAcc(this.id!, {locked: state}).subscribe((res) => {
if (res.status == 200) {
this.locked = state
}
}) })
} }
} }

View File

@@ -1,6 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserResetComponent } from './user-reset.component'; import { UserResetComponent } from './user-reset.component';
import { MatDialogModule } from '@angular/material/dialog';
describe('UserResetComponent', () => { describe('UserResetComponent', () => {
let component: UserResetComponent; let component: UserResetComponent;
@@ -8,7 +9,10 @@ describe('UserResetComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [UserResetComponent] declarations: [UserResetComponent],
imports: [
MatDialogModule
]
}); });
fixture = TestBed.createComponent(UserResetComponent); fixture = TestBed.createComponent(UserResetComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@@ -11,6 +11,7 @@ import { News } from '../types/news';
import { AKey } from '../types/key'; import { AKey } from '../types/key';
import * as moment from 'moment'; import * as moment from 'moment';
import { IUSettings } from './settings/settings.component'; import { IUSettings } from './settings/settings.component';
import User from '../types/user';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@@ -121,17 +122,7 @@ export class AdminCommService {
accs = { accs = {
getAccs: () => { getAccs: () => {
return this.http.get<{ return this.http.get<{
users: { users: Omit<User, "pass">[],
_id: string;
uname: string;
pass: string;
room?: string;
admin?: number;
locked?: boolean;
fname?: string;
surname?: string;
groups: string[];
}[],
groups: Group[] groups: Group[]
}>(environment.apiEndpoint+`/admin/accs`, {withCredentials: true}) }>(environment.apiEndpoint+`/admin/accs`, {withCredentials: true})
}, },
@@ -140,7 +131,7 @@ export class AdminCommService {
return this.http.post<Status>(environment.apiEndpoint+`/admin/accs`, item, {withCredentials: true}) return this.http.post<Status>(environment.apiEndpoint+`/admin/accs`, item, {withCredentials: true})
}, },
putAcc: (id: string, update: object) => { putAcc: (id: string, update: Partial<User>) => {
return this.http.put<Status>(environment.apiEndpoint+`/admin/accs/${id}`, update, {withCredentials: true}) return this.http.put<Status>(environment.apiEndpoint+`/admin/accs/${id}`, update, {withCredentials: true})
}, },
@@ -150,6 +141,14 @@ export class AdminCommService {
deleteAcc: (id: string) => { deleteAcc: (id: string) => {
return this.http.delete<Status>(environment.apiEndpoint+`/admin/accs/${id}`, {withCredentials: true}) return this.http.delete<Status>(environment.apiEndpoint+`/admin/accs/${id}`, {withCredentials: true})
},
getUser: (id: string) => {
return this.http.get<Omit<User, "pass"> & {lockout: boolean}>(environment.apiEndpoint+`/admin/accs/${id}`, {withCredentials: true})
},
clearLockout: (id: string) => {
return this.http.delete<Status>(environment.apiEndpoint+`/admin/accs/${id}/lockout`, {withCredentials: true})
} }
} }
//#endregion //#endregion
@@ -183,6 +182,17 @@ export class AdminCommService {
}, },
getGroups: () => { getGroups: () => {
return this.http.get<Group[]>(environment.apiEndpoint+"/admin/notif/groups", {withCredentials: true}) return this.http.get<Group[]>(environment.apiEndpoint+"/admin/notif/groups", {withCredentials: true})
},
outbox: {
getSent: () => {
return this.http.get<{_id: string, sentDate: moment.Moment, title: string}[]>(environment.apiEndpoint+"/admin/notif/outbox", {withCredentials: true})
},
getBody: (id: string) => {
return this.http.get(environment.apiEndpoint+`/admin/notif/outbox/${id}/message`, {withCredentials: true, responseType: "text"})
},
getRcpts: (id: string) => {
return this.http.get<{_id: string, uname: string, room?: string, fname?: string, surname?: string}[]>(environment.apiEndpoint+`/admin/notif/outbox/${id}/rcpts`, {withCredentials: true})
}
} }
} }
//#endregion //#endregion
@@ -236,13 +246,16 @@ export class AdminCommService {
}, },
attendence: { attendence: {
getUsers: (room: string) => { getUsers: (room: string) => {
return this.http.get<{users: {fname: string, surname: string, _id: string}[], attendence?: {id: string, hour?: string}[]}>(environment.apiEndpoint+`/admin/clean/attendence/${room}`, {withCredentials: true}) return this.http.get<{users: {fname: string, surname: string, _id: string}[], attendence?: {auto: {id: string, hour?: string}[], notes: string}}>(environment.apiEndpoint+`/admin/clean/attendence/${room}`, {withCredentials: true})
}, },
postAttendence: (room: string, attendence: {id: string, hour?: string}[]) => { postAttendence: (room: string, attendence: {auto: {id: string, hour?: string}[], notes: string}) => {
return this.http.post<Status>(environment.apiEndpoint+`/admin/clean/attendence/${room}`, attendence, {withCredentials: true}) return this.http.post<Status>(environment.apiEndpoint+`/admin/clean/attendence/${room}`, attendence, {withCredentials: true})
}, },
getSummary: () => { getSummary: () => {
return this.http.get<{room: string, hours: string[]}[]>(environment.apiEndpoint+`/admin/clean/attendenceSummary`, {withCredentials: true}) return this.http.get<{room: string, hours: string[], notes: string, auto: boolean}[]>(environment.apiEndpoint+`/admin/clean/attendenceSummary`, {withCredentials: true})
},
deleteRoom: (room: string) => {
return this.http.delete<Status>(environment.apiEndpoint+`/admin/clean/attendence/${room}`, {withCredentials: true})
} }
} }
} }

View File

@@ -1,17 +1,4 @@
<mat-toolbar color="accent"> <app-toolbar [drawer]="drawer"/>
<button mat-icon-button (click)="drawer.toggle()"><mat-icon>menu</mat-icon></button>
<span>{{title.getTitle()}}</span>
<span style="flex: 1 1 auto"></span>
<button mat-icon-button *ngIf="toolbar.menu" [matMenuTriggerFor]="menu"><mat-icon>more_vert</mat-icon></button>
</mat-toolbar>
<mat-menu #menu="matMenu">
@for (item of toolbar.menu; track $index) {
<button mat-menu-item *ngIf="item.check" (click)="toolbar.comp[item.fn]()">
<mat-icon *ngIf="item.icon">{{item.icon}}</mat-icon>
<span>{{item.title}}</span>
</button>
}
</mat-menu>
<mat-sidenav-container> <mat-sidenav-container>
<mat-sidenav #drawer mode="over" autoFocus="false"> <mat-sidenav #drawer mode="over" autoFocus="false">
<mat-nav-list> <mat-nav-list>
@@ -21,6 +8,10 @@
<a matListItemTitle>{{link.title}}</a> <a matListItemTitle>{{link.title}}</a>
</mat-list-item> </mat-list-item>
} }
<a mat-list-item href="https://foliand.men/wiki/!ipwa/" target="_blank">
<mat-icon matListItemIcon>developer_guide</mat-icon>
<a matListItemTitle>Dokumentacja</a>
</a>
<mat-list-item (click)="goNormal()"> <mat-list-item (click)="goNormal()">
<mat-icon matListItemIcon>close</mat-icon> <mat-icon matListItemIcon>close</mat-icon>
<h4 matListItemTitle>Zakończ edycję</h4> <h4 matListItemTitle>Zakończ edycję</h4>

View File

@@ -3,10 +3,16 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AdminViewComponent } from './admin-view.component'; import { AdminViewComponent } from './admin-view.component';
import { MatToolbarModule } from '@angular/material/toolbar'; import { MatToolbarModule } from '@angular/material/toolbar';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatSidenavModule } from '@angular/material/sidenav'; import { MatDrawer, MatSidenavModule } from '@angular/material/sidenav';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatListModule } from '@angular/material/list'; import { MatListModule } from '@angular/material/list';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { Component, Input } from '@angular/core';
@Component({selector: "app-toolbar", template: ''})
class ToolbarMock {
@Input() drawer!: MatDrawer;
}
describe('AdminViewComponent', () => { describe('AdminViewComponent', () => {
let component: AdminViewComponent; let component: AdminViewComponent;
@@ -14,7 +20,7 @@ describe('AdminViewComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [AdminViewComponent], declarations: [AdminViewComponent, ToolbarMock],
imports: [MatToolbarModule, MatIconModule, MatSidenavModule, BrowserAnimationsModule, MatListModule, RouterModule.forRoot([])] imports: [MatToolbarModule, MatIconModule, MatSidenavModule, BrowserAnimationsModule, MatListModule, RouterModule.forRoot([])]
}); });
fixture = TestBed.createComponent(AdminViewComponent); fixture = TestBed.createComponent(AdminViewComponent);

View File

@@ -1,9 +1,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { LocalStorageService } from '../services/local-storage.service'; import { LocalStorageService } from '../services/local-storage.service';
import { Link } from '../types/link'; import { Link } from '../types/link';
import { ToolbarService } from './toolbar.service';
@Component({ @Component({
selector: 'app-admin-view', selector: 'app-admin-view',
@@ -21,12 +19,11 @@ export class AdminViewComponent {
{ title: "Czystość", icon: "cleaning_services", href: "grades", enabled: this.ls.permChecker(128) && this.ls.capCheck(16) }, { title: "Czystość", icon: "cleaning_services", href: "grades", enabled: this.ls.permChecker(128) && this.ls.capCheck(16) },
{ title: "Frekwencja", icon: "checklist", href: "attendence", enabled: false }, { title: "Frekwencja", icon: "checklist", href: "attendence", enabled: false },
{ title: "Ustawienia", icon: "settings_applications", href: "settings", enabled: this.ls.permChecker(32) }, { title: "Ustawienia", icon: "settings_applications", href: "settings", enabled: this.ls.permChecker(32) },
{ title: "Instrukcje", icon: "description", href: "guide", enabled: true }
]; ];
public get LINKS(): Link[] { public get LINKS(): Link[] {
return this._LINKS.filter(v => v.enabled); return this._LINKS.filter(v => v.enabled);
} }
constructor(readonly title: Title, readonly router: Router, readonly ls: LocalStorageService, protected toolbar: ToolbarService) { } constructor(readonly router: Router, readonly ls: LocalStorageService) { }
goNormal() { goNormal() {
this.router.navigateByUrl('app') this.router.navigateByUrl('app')
} }

View File

@@ -14,7 +14,11 @@
</div> </div>
<div matColumnDef="hours"> <div matColumnDef="hours">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Godziny</th> <th mat-header-cell *matHeaderCellDef mat-sort-header>Godziny</th>
<td mat-cell *matCellDef="let item"><span *ngFor="let i of item.hours.sort().reverse(); let isLast=last"><app-hour-display [value]="i"></app-hour-display>{{ isLast ? '' : ', '}}</span></td> <td mat-cell *matCellDef="let item"><span *ngFor="let i of item.hours.sort().reverse(); let isLast=last"><app-hour-display [value]="i"></app-hour-display>{{ isLast ? '' : ', '}}</span><span>{{item.notes}}</span></td>
</div>
<div matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Usuń</th>
<td mat-cell *matCellDef="let item"><button mat-mini-fab color="warn" (click)="delete(item.room)" *ngIf="!item.auto"><mat-icon>delete</mat-icon></button></td>
</div> </div>
<tr mat-header-row *matHeaderRowDef="collumns"></tr> <tr mat-header-row *matHeaderRowDef="collumns"></tr>
<tr mat-row *matRowDef="let rowData; columns: collumns"></tr> <tr mat-row *matRowDef="let rowData; columns: collumns"></tr>

View File

@@ -1,14 +1,33 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AttendenceSummaryComponent } from './attendence-summary.component'; import { AttendenceSummaryComponent } from './attendence-summary.component';
import { RouterModule } from '@angular/router';
import { AdminCommService } from '../../admin-comm.service';
import { of } from 'rxjs';
import { MatTableModule } from '@angular/material/table';
describe('AttendenceSummaryComponent', () => { describe('AttendenceSummaryComponent', () => {
let component: AttendenceSummaryComponent; let component: AttendenceSummaryComponent;
let fixture: ComponentFixture<AttendenceSummaryComponent>; let fixture: ComponentFixture<AttendenceSummaryComponent>;
let acMock
beforeEach(async () => { beforeEach(async () => {
acMock = {
clean: {
attendence: {
getSummary: jasmine.createSpy("getSummary").and.returnValue(of())
}
}
}
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [AttendenceSummaryComponent] declarations: [AttendenceSummaryComponent],
imports: [
RouterModule.forRoot([]),
MatTableModule
],
providers: [
{provide: AdminCommService, useValue: acMock}
]
}) })
.compileComponents(); .compileComponents();

View File

@@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ToolbarService } from '../../toolbar.service'; import { ToolbarService } from '../../toolbar/toolbar.service';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { AdminCommService } from '../../admin-comm.service'; import { AdminCommService } from '../../admin-comm.service';
@@ -11,8 +11,8 @@ import { AdminCommService } from '../../admin-comm.service';
}) })
export class AttendenceSummaryComponent implements OnInit { export class AttendenceSummaryComponent implements OnInit {
data: MatTableDataSource<{room: string, hours: string[]}> = new MatTableDataSource<{room: string, hours: string[]}>(); data: MatTableDataSource<{room: string, hours: string[], notes: string, auto: boolean}> = new MatTableDataSource<{room: string, hours: string[], notes: string, auto: boolean}>();
collumns = ['room', 'hours'] collumns = ['room', 'hours', 'actions']
constructor (private toolbar: ToolbarService, private router: Router, private route: ActivatedRoute, private ac: AdminCommService) { constructor (private toolbar: ToolbarService, private router: Router, private route: ActivatedRoute, private ac: AdminCommService) {
this.toolbar.comp = this this.toolbar.comp = this
@@ -21,6 +21,12 @@ export class AttendenceSummaryComponent implements OnInit {
] ]
} }
delete(room: string) {
this.ac.clean.attendence.deleteRoom(room).subscribe(() => {
this.ngOnInit()
})
}
ngOnInit(): void { ngOnInit(): void {
this.ac.clean.attendence.getSummary().subscribe(v => { this.ac.clean.attendence.getSummary().subscribe(v => {
this.data.data = v this.data.data = v

View File

@@ -11,6 +11,10 @@
</div> </div>
} }
</div> </div>
<mat-form-field>
<mat-label>Notatki</mat-label>
<input type="text" matInput formControlName="notes">
</mat-form-field>
</form> </form>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions> <mat-dialog-actions>

View File

@@ -1,14 +1,41 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AttendenceComponent } from './attendence.component'; import { AttendenceComponent } from './attendence.component';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { AdminCommService } from '../../admin-comm.service';
import { MatFormFieldModule } from '@angular/material/form-field';
import { of } from 'rxjs';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('AttendenceComponent', () => { describe('AttendenceComponent', () => {
let component: AttendenceComponent; let component: AttendenceComponent;
let fixture: ComponentFixture<AttendenceComponent>; let fixture: ComponentFixture<AttendenceComponent>;
beforeEach(async () => { beforeEach(async () => {
const acMock = {
clean: {
attendence: {
getUsers: jasmine.createSpy("getUsers").and.returnValue(of())
}
}
}
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [AttendenceComponent] declarations: [AttendenceComponent],
providers: [
{provide: MAT_DIALOG_DATA, useValue: {}},
{provide: MatDialogRef, useValue: {}},
{provide: AdminCommService, useValue: acMock}
],
imports: [
MatDialogModule,
MatFormFieldModule,
FormsModule,
ReactiveFormsModule,
MatInputModule,
NoopAnimationsModule
]
}) })
.compileComponents(); .compileComponents();

View File

@@ -10,13 +10,13 @@ import { AdminCommService } from '../../admin-comm.service';
}) })
export class AttendenceComponent implements OnInit { export class AttendenceComponent implements OnInit {
constructor (private fb: FormBuilder, @Inject(MAT_DIALOG_DATA) public data: {room: string}, public dialogRef: MatDialogRef<AttendenceComponent>, private ac: AdminCommService) {} constructor(private fb: FormBuilder, @Inject(MAT_DIALOG_DATA) public data: { room: string }, public dialogRef: MatDialogRef<AttendenceComponent>, private ac: AdminCommService) { }
ngOnInit(): void { ngOnInit(): void {
this.room = this.data.room this.room = this.data.room
this.ac.clean.attendence.getUsers(this.room).subscribe(query => { this.ac.clean.attendence.getUsers(this.room).subscribe(query => {
query.users.forEach(v => { query.users.forEach(v => {
var att = query.attendence ? query.attendence.find(z => z.id == v._id) : false var att = query.attendence ? query.attendence.auto.find(z => z.id == v._id) : false
this.users.push(this.fb.group({ this.users.push(this.fb.group({
id: v._id, id: v._id,
label: `${v.fname} ${v.surname}`, label: `${v.fname} ${v.surname}`,
@@ -24,6 +24,7 @@ export class AttendenceComponent implements OnInit {
hour: this.fb.control(att ? att.hour : ""), hour: this.fb.control(att ? att.hour : ""),
})) }))
}) })
this.form.get('notes')?.setValue(query.attendence?.notes)
}) })
} }
@@ -37,7 +38,8 @@ export class AttendenceComponent implements OnInit {
room: string = ""; room: string = "";
form: FormGroup = this.fb.group({ form: FormGroup = this.fb.group({
users: this.fb.array([]) users: this.fb.array([]),
notes: this.fb.control(""),
}) })
get users() { get users() {

View File

@@ -1,14 +1,55 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GradesComponent } from './grades.component'; import { GradesComponent } from './grades.component';
import { AdminCommService } from '../admin-comm.service';
import { RouterModule } from '@angular/router';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import * as moment from 'moment';
import { MatIconModule } from '@angular/material/icon';
import { MatFormFieldModule } from '@angular/material/form-field';
import { of } from 'rxjs';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
@Component({selector: "app-date-selector", template: ''})
class DateSelectorStub {
@Input() date: moment.Moment = moment.utc().startOf('day');
@Output() dateChange = new EventEmitter<moment.Moment>();
@Input() filter: (date: moment.Moment | null) => boolean = () => true
}
@Component({selector: "app-room-chooser", template: ''})
class RoomSelectorStub {
@Input() rooms: string[] = []
@Output() room: EventEmitter<string> = new EventEmitter<string>();
}
describe('GradesComponent', () => { describe('GradesComponent', () => {
let component: GradesComponent; let component: GradesComponent;
let fixture: ComponentFixture<GradesComponent>; let fixture: ComponentFixture<GradesComponent>;
let acMock
beforeEach(async () => { beforeEach(async () => {
acMock = {
clean: {
getConfig: jasmine.createSpy("getConfig").and.returnValue(of())
}
}
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [GradesComponent] declarations: [GradesComponent, DateSelectorStub, RoomSelectorStub],
providers: [
{provide: AdminCommService, useValue: acMock}
],
imports: [
RouterModule.forRoot([]),
MatIconModule,
MatFormFieldModule,
FormsModule,
ReactiveFormsModule,
MatInputModule,
NoopAnimationsModule
]
}) })
.compileComponents(); .compileComponents();

View File

@@ -4,7 +4,7 @@ import * as moment from 'moment';
import { FormArray, FormBuilder } from '@angular/forms'; import { FormArray, FormBuilder } from '@angular/forms';
import { weekendFilter } from 'src/app/fd.da'; import { weekendFilter } from 'src/app/fd.da';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { ToolbarService } from '../toolbar.service'; import { ToolbarService } from '../toolbar/toolbar.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { AttendenceComponent } from './attendence/attendence.component'; import { AttendenceComponent } from './attendence/attendence.component';
@@ -49,8 +49,8 @@ export class GradesComponent implements OnInit, OnDestroy {
if (!this.filter(this.date)) this.date.isoWeekday(8); if (!this.filter(this.date)) this.date.isoWeekday(8);
this.toolbar.comp = this this.toolbar.comp = this
this.toolbar.menu = [ this.toolbar.menu = [
{ title: "Pokoje do sprawdzenia", check: true, fn: "attendenceSummary", icon: "overview"},
{ title: "Podsumowanie", check: true, fn: "summary", icon: "analytics" }, { title: "Podsumowanie", check: true, fn: "summary", icon: "analytics" },
{ title: "Obecność", check: true, fn: "attendenceSummary", icon: "overview"}
] ]
this.form.valueChanges.subscribe((v) => { this.form.valueChanges.subscribe((v) => {
this.calculate() this.calculate()
@@ -164,7 +164,8 @@ export class GradesComponent implements OnInit, OnDestroy {
} }
attendence() { attendence() {
this.dialog.open(AttendenceComponent, {data: {room: this.room}}).afterClosed().subscribe((v: {room: string, users: {att: boolean, id: string, hour: string}[]}) => { this.dialog.open(AttendenceComponent, {data: {room: this.room}}).afterClosed().subscribe((v: {room: string, users: {att: boolean, id: string, hour: string}[], notes: string}) => {
if (!v) return
let x: {room: string, users: {id: string, hour?: string}[]} = { let x: {room: string, users: {id: string, hour?: string}[]} = {
room: v.room, room: v.room,
users: [] users: []
@@ -174,7 +175,7 @@ export class GradesComponent implements OnInit, OnDestroy {
x.users.push({id: i.id, hour: i.hour}) x.users.push({id: i.id, hour: i.hour})
} }
}) })
this.ac.clean.attendence.postAttendence(x.room, x.users).subscribe((s) => { this.ac.clean.attendence.postAttendence(x.room, {auto: x.users, notes: v.notes}).subscribe((s) => {
if (s.status == 200) { if (s.status == 200) {
this.sb.open("Zapisano obecność!", undefined, {duration: 1500}) this.sb.open("Zapisano obecność!", undefined, {duration: 1500})
} }

View File

@@ -1,17 +1,52 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SummaryComponent } from './summary.component'; import { SummaryComponent } from './summary.component';
import { RouterModule } from '@angular/router';
import { AdminCommService } from '../../admin-comm.service';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MAT_MOMENT_DATE_ADAPTER_OPTIONS, MAT_MOMENT_DATE_FORMATS, MomentDateAdapter, provideMomentDateAdapter } from '@angular/material-moment-adapter';
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core';
import { MatIconModule } from '@angular/material/icon';
import { of } from 'rxjs';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatTableModule } from '@angular/material/table';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('SummaryComponent', () => { describe('SummaryComponent', () => {
let component: SummaryComponent; let component: SummaryComponent;
let fixture: ComponentFixture<SummaryComponent>; let fixture: ComponentFixture<SummaryComponent>;
beforeEach(async () => { beforeEach(async () => {
const acMock = {
clean: {
summary: {
getSummary: jasmine.createSpy("getSummary").and.returnValue(of())
}
}
}
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [SummaryComponent] declarations: [SummaryComponent],
providers: [
{ provide: AdminCommService, useValue: acMock },
{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE, MAT_DATE_FORMATS, MAT_MOMENT_DATE_ADAPTER_OPTIONS] },
{ provide: MAT_DATE_LOCALE, useValue: "pl-PL" },
{ provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS },
{ provide: MAT_MOMENT_DATE_ADAPTER_OPTIONS, useValue: { useUtc: true } },
],
imports: [
RouterModule.forRoot([]),
MatFormFieldModule,
MatDatepickerModule,
MatIconModule,
FormsModule,
ReactiveFormsModule,
MatTableModule,
NoopAnimationsModule
]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(SummaryComponent); fixture = TestBed.createComponent(SummaryComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();

View File

@@ -1,5 +1,5 @@
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ToolbarService } from '../../toolbar.service'; import { ToolbarService } from '../../toolbar/toolbar.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { AdminCommService } from '../../admin-comm.service'; import { AdminCommService } from '../../admin-comm.service';
import * as moment from 'moment'; import * as moment from 'moment';

View File

@@ -1,14 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GroupsComponent } from './groups.component'; import { GroupsComponent } from './groups.component';
import { AdminCommService } from '../admin-comm.service';
import { of } from 'rxjs';
describe('GroupsComponent', () => { describe('GroupsComponent', () => {
let component: GroupsComponent; let component: GroupsComponent;
let fixture: ComponentFixture<GroupsComponent>; let fixture: ComponentFixture<GroupsComponent>;
beforeEach(() => { beforeEach(() => {
const acMock = {
groups: {
getGroups: jasmine.createSpy("getGroups").and.returnValue(of())
}
}
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [GroupsComponent] declarations: [GroupsComponent],
providers: [
{provide: AdminCommService, useValue: acMock}
]
}); });
fixture = TestBed.createComponent(GroupsComponent); fixture = TestBed.createComponent(GroupsComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@@ -1,6 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RemoveConfirmComponent } from './remove-confirm.component'; import { RemoveConfirmComponent } from './remove-confirm.component';
import { MatDialogModule } from '@angular/material/dialog';
describe('RemoveConfirmComponent', () => { describe('RemoveConfirmComponent', () => {
let component: RemoveConfirmComponent; let component: RemoveConfirmComponent;
@@ -8,7 +9,8 @@ describe('RemoveConfirmComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [RemoveConfirmComponent] declarations: [RemoveConfirmComponent],
imports: [MatDialogModule]
}); });
fixture = TestBed.createComponent(RemoveConfirmComponent); fixture = TestBed.createComponent(RemoveConfirmComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@@ -8,7 +8,7 @@
</mat-chip-listbox> </mat-chip-listbox>
<button mat-icon-button (click)="new()"><mat-icon>add</mat-icon></button> <button mat-icon-button (click)="new()"><mat-icon>add</mat-icon></button>
</div> </div>
<mat-spinner *ngIf="loading"></mat-spinner> <mat-spinner *ngIf="loading" color="accent"></mat-spinner>
<table mat-table [dataSource]="keys"> <table mat-table [dataSource]="keys">
<div matColumnDef="room"> <div matColumnDef="room">
<th mat-header-cell *matHeaderCellDef>Sala</th> <th mat-header-cell *matHeaderCellDef>Sala</th>

View File

@@ -1,14 +1,35 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AdminKeyComponent } from './key.component'; import { AdminKeyComponent } from './key.component';
import { of } from 'rxjs';
import { AdminCommService } from '../admin-comm.service';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatChipsModule } from '@angular/material/chips';
import { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule } from '@angular/material/paginator';
import { FormsModule } from '@angular/forms';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatTableModule } from '@angular/material/table';
import { MatInputModule } from '@angular/material/input';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('KeyComponent', () => { describe('AdminKeyComponent', () => {
let component: AdminKeyComponent; let component: AdminKeyComponent;
let fixture: ComponentFixture<AdminKeyComponent>; let fixture: ComponentFixture<AdminKeyComponent>;
let acMock
beforeEach(async () => { beforeEach(async () => {
acMock = {
keys: {
getKeys: jasmine.createSpy("getKeys").and.returnValue(of())
}
}
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [AdminKeyComponent] declarations: [AdminKeyComponent],
providers: [
{provide: AdminCommService, useValue: acMock}
],
imports: [MatFormFieldModule, MatChipsModule, MatIconModule, MatPaginatorModule, FormsModule, MatProgressSpinnerModule, MatTableModule, MatInputModule, NoopAnimationsModule]
}) })
.compileComponents(); .compileComponents();

View File

@@ -1,6 +1,6 @@
<mat-dialog-content> <mat-dialog-content>
<form (ngSubmit)="send()" [formGroup]="form"> <form (ngSubmit)="send()" [formGroup]="form">
<mat-form-field> <mat-form-field color="accent">
<mat-label>Sala</mat-label> <mat-label>Sala</mat-label>
<mat-select formControlName="room" required> <mat-select formControlName="room" required>
@for (item of rooms; track $index) { @for (item of rooms; track $index) {
@@ -9,17 +9,9 @@
</mat-select> </mat-select>
<mat-error *ngIf="form.controls['room'].hasError('required')">Wymagane</mat-error> <mat-error *ngIf="form.controls['room'].hasError('required')">Wymagane</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field color="accent">
<mat-label>Wypożyczający</mat-label> <mat-label>Wypożyczający</mat-label>
<!-- TODO: Add user selector --> <app-user-search formControlName="user" required/>
<input matInput placeholder="Nazwa użytkownika" formControlName="user" required>
<!-- <input #input matInput placeholder="Nazwa użytkownika" formControlName="user" required [matAutocomplete]="auto" (input)="filter()">
<mat-autocomplete requireSelection #auto="matAutocomplete">
@for (item of unames; track item) {
<mat-option [value]="item">{{item}}</mat-option>
}
</mat-autocomplete> -->
<mat-error *ngIf="form.controls['user'].hasError('unf')">Zła nazwa użytkownika</mat-error>
<mat-error *ngIf="form.controls['user'].hasError('required')">Wymagane</mat-error> <mat-error *ngIf="form.controls['user'].hasError('required')">Wymagane</mat-error>
</mat-form-field> </mat-form-field>
<button mat-button>Wyślij</button> <button mat-button>Wyślij</button>

View File

@@ -1,17 +1,77 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NewKeyComponent } from './new-key.component'; import { NewKeyComponent } from './new-key.component';
import { AdminCommService } from '../../admin-comm.service';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldControl, MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { Component, forwardRef, Optional, Self } from '@angular/core';
import { Observable, of } from 'rxjs';
import { AbstractControlDirective, ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, NgControl, ReactiveFormsModule } from '@angular/forms';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
@Component({
selector: "app-user-search", template: '', providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => UserSearchStub),
multi: true,
},
{
provide: MatFormFieldControl,
useExisting: UserSearchStub
}]
})
class UserSearchStub implements ControlValueAccessor, MatFormFieldControl<never> {
value: null = null;
stateChanges: Observable<void> = of();
id: string = "";
placeholder: string = "";
ngControl: NgControl | AbstractControlDirective | null = null;
focused: boolean = false;
empty: boolean = true;
shouldLabelFloat: boolean = true;
required: boolean = false;
disabled: boolean = false;
errorState: boolean = false;
controlType?: string | undefined;
autofilled?: boolean | undefined;
userAriaDescribedBy?: string | undefined;
setDescribedByIds(ids: string[]): void {}
onContainerClick(event: MouseEvent): void {}
writeValue(obj: any): void {}
registerOnChange(fn: any): void {}
registerOnTouched(fn: any): void {}
setDisabledState?(isDisabled: boolean): void {}
}
describe('NewKeyComponent', () => { describe('NewKeyComponent', () => {
let component: NewKeyComponent; let component: NewKeyComponent;
let fixture: ComponentFixture<NewKeyComponent>; let fixture: ComponentFixture<NewKeyComponent>;
let acMock
beforeEach(async () => { beforeEach(async () => {
acMock = {
keys: {
avalKeys: jasmine.createSpy("avalKeys").and.returnValue(of())
}
}
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [NewKeyComponent] declarations: [NewKeyComponent, UserSearchStub],
providers: [
{ provide: AdminCommService, useValue: acMock },
{ provide: MatDialogRef, useValue: {} }
],
imports: [
MatDialogModule,
MatFormFieldModule,
MatSelectModule,
FormsModule,
ReactiveFormsModule,
NoopAnimationsModule
]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(NewKeyComponent); fixture = TestBed.createComponent(NewKeyComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();

View File

@@ -1,8 +1,8 @@
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { AdminCommService } from '../../admin-comm.service'; import { AdminCommService } from '../../admin-comm.service';
import { MatDialogRef } from '@angular/material/dialog'; import { MatDialogRef } from '@angular/material/dialog';
import { FormControl, FormGroup } from '@angular/forms'; import { FormControl, FormGroup } from '@angular/forms';
import { startWith } from 'rxjs'; import { UserSearchResult } from 'src/app/commonComponents/user-search/user-search.component';
@Component({ @Component({
selector: 'app-new-key', selector: 'app-new-key',
@@ -10,11 +10,10 @@ import { startWith } from 'rxjs';
styleUrl: './new-key.component.scss' styleUrl: './new-key.component.scss'
}) })
export class NewKeyComponent implements OnInit { export class NewKeyComponent implements OnInit {
// @ViewChild('input') input!: ElementRef<HTMLInputElement>
rooms: string[] = [] rooms: string[] = []
form = new FormGroup({ form = new FormGroup({
room: new FormControl<string>(""), room: new FormControl<string>(""),
user: new FormControl<string>("") user: new FormControl<UserSearchResult | null>(null)
}) })
unames: any[] = [] unames: any[] = []
constructor ( private ac: AdminCommService, public dialogRef: MatDialogRef<NewKeyComponent> ) {} constructor ( private ac: AdminCommService, public dialogRef: MatDialogRef<NewKeyComponent> ) {}
@@ -24,26 +23,11 @@ export class NewKeyComponent implements OnInit {
this.rooms = v this.rooms = v
}) })
} }
// filter() {
// const v = this.input.nativeElement.value
// console.log(v);
// if (v) {
// this.ac.userFilter(v.toLowerCase()).subscribe((v) => {
// this.unames = v
// })
// } else {
// this.unames = []
// }
// }
send() { send() {
if (this.form.valid) { if (this.form.valid) {
this.dialogRef.close(this.form.value) this.dialogRef.close(this.form.value)
} else { }
this.form.controls['user'].setErrors({unf: true})
}
} }
} }

View File

@@ -1,4 +1,4 @@
<h1 mat-dialog-title>Dodawanie</h1> <h1 mat-dialog-title>Tworzenie wpisów do jadłospisu</h1>
<mat-dialog-content> <mat-dialog-content>
<mat-radio-group [(ngModel)]="type"> <mat-radio-group [(ngModel)]="type">
<mat-radio-button value="day">Dzień</mat-radio-button> <mat-radio-button value="day">Dzień</mat-radio-button>
@@ -28,6 +28,6 @@
</div> </div>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions> <mat-dialog-actions>
<button mat-raised-button color="accent" (click)="submit()">Wyślij</button> <button mat-raised-button color="accent" (click)="submit()">Utwórz pozycje</button>
<button mat-button mat-dialog-close>Anuluj</button> <button mat-button mat-dialog-close>Anuluj</button>
</mat-dialog-actions> </mat-dialog-actions>

View File

@@ -1,6 +1,9 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MenuAddComponent } from './menu-add.component'; import { MenuAddComponent } from './menu-add.component';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatRadioModule } from '@angular/material/radio';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
describe('MenuAddComponent', () => { describe('MenuAddComponent', () => {
let component: MenuAddComponent; let component: MenuAddComponent;
@@ -8,7 +11,17 @@ describe('MenuAddComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [MenuAddComponent] declarations: [MenuAddComponent],
providers: [
{provide: MAT_DIALOG_DATA, useValue: {}},
{provide: MatDialogRef, useValue: {}}
],
imports: [
MatDialogModule,
MatRadioModule,
ReactiveFormsModule,
FormsModule
]
}) })
.compileComponents(); .compileComponents();

View File

@@ -31,7 +31,7 @@ export class MenuAddComponent {
submit() { submit() {
switch (this.type) { switch (this.type) {
case "day": case "day":
this.dialogRef.close({type: "day", value: this.day.utc()}) this.dialogRef.close({type: "day", value: this.day.utc().startOf('day')})
break; break;
case "week": case "week":
this.dialogRef.close({type: "week", value: {start: this.range.value.start?.utc().hours(24), count: 5}}) this.dialogRef.close({type: "week", value: {start: this.range.value.start?.utc().hours(24), count: 5}})

View File

@@ -1,4 +1,4 @@
import { AfterViewInit, Component, ViewChild } from '@angular/core'; import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms'; import { FormControl, FormGroup } from '@angular/forms';
import { MAT_DATE_RANGE_SELECTION_STRATEGY } from '@angular/material/datepicker'; import { MAT_DATE_RANGE_SELECTION_STRATEGY } from '@angular/material/datepicker';
import { Moment } from 'moment'; import { Moment } from 'moment';
@@ -54,10 +54,10 @@ export class MenuNewComponent {
if (data) { if (data) {
switch (data.type) { switch (data.type) {
case "day": case "day":
this.ac.menu.new.single(data.value).subscribe() this.ac.menu.new.single(data.value).subscribe(s => this.refreshIfGood(s))
break; break;
case "week": case "week":
this.ac.menu.new.range(data.value.start, data.value.count).subscribe() this.ac.menu.new.range(data.value.start, data.value.count).subscribe(s => this.refreshIfGood(s))
break; break;
case "file": case "file":
this.requestData() this.requestData()
@@ -87,7 +87,7 @@ export class MenuNewComponent {
} }
private refreshIfGood(s: Status) { private refreshIfGood(s: Status) {
if (s.status == 200) { if (s.status.toString().match(/2\d\d/)) {
this.requestData() this.requestData()
} }
} }
@@ -100,25 +100,20 @@ export class MenuNewComponent {
}) })
} }
editDay(v: string | string[], element: Menu) {
v = v as string
element.day = moment(v, "DD.MM.YYYY", true).utc(true).startOf('day')
}
editSn(id: string) { editSn(id: string) {
this.ac.menu.editSn(id, this.dataSource.data.find(v => v._id == id)?.sn).subscribe(this.refreshIfGood) this.ac.menu.editSn(id, this.dataSource.data.find(v => v._id == id)!.sn).subscribe(s => this.refreshIfGood(s))
} }
editOb(id: string) { editOb(id: string) {
this.ac.menu.editOb(id, this.dataSource.data.find(v => v._id == id)?.ob).subscribe(this.refreshIfGood) this.ac.menu.editOb(id, this.dataSource.data.find(v => v._id == id)!.ob).subscribe(s => this.refreshIfGood(s))
} }
editKol(id: string) { editKol(id: string) {
this.ac.menu.editKol(id, this.dataSource.data.find(v => v._id == id)?.kol).subscribe(this.refreshIfGood) this.ac.menu.editKol(id, this.dataSource.data.find(v => v._id == id)?.kol).subscribe(s => this.refreshIfGood(s))
} }
editTitle(id: string) { editTitle(id: string) {
this.ac.menu.editTitle(id, this.dataSource.data.find(v => v._id == id)?.dayTitle).subscribe(this.refreshIfGood) this.ac.menu.editTitle(id, this.dataSource.data.find(v => v._id == id)?.dayTitle).subscribe(s => this.refreshIfGood(s))
} }
getStat(day: moment.Moment, m: "ob" | "kol") { getStat(day: moment.Moment, m: "ob" | "kol") {
@@ -126,6 +121,6 @@ export class MenuNewComponent {
} }
remove(id: string) { remove(id: string) {
this.ac.menu.rm(id).subscribe(this.refreshIfGood) this.ac.menu.rm(id).subscribe(s => this.refreshIfGood(s))
} }
} }

View File

@@ -22,4 +22,9 @@
<mat-card-footer> <mat-card-footer>
<p>{{item.date | date:'d-LL-yyyy HH:mm'}}</p> <p>{{item.date | date:'d-LL-yyyy HH:mm'}}</p>
</mat-card-footer> </mat-card-footer>
</mat-card>
<mat-card *ngIf="news.length == 0">
<p>
Brak wiadomości.
</p>
</mat-card> </mat-card>

View File

@@ -22,6 +22,10 @@ mat-card-content p {
white-space: pre-line; white-space: pre-line;
} }
mat-card p {
margin: 15px;
}
button { button {
margin-right: 4pt; margin-right: 4pt;
} }

View File

@@ -5,21 +5,25 @@ import { AdminCommService } from '../admin-comm.service';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSnackBarModule } from '@angular/material/snack-bar';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { MatCardModule } from '@angular/material/card';
describe('NewsEditComponent', () => { describe('NewsEditComponent', () => {
let component: NewsEditComponent; let component: NewsEditComponent;
let fixture: ComponentFixture<NewsEditComponent>; let fixture: ComponentFixture<NewsEditComponent>;
let acMock
beforeEach(() => { beforeEach(() => {
const acMock = jasmine.createSpyObj('AdminCommService', { acMock = {
getNews: of() news: {
}) getNews: jasmine.createSpy('getNews').and.returnValue(of([]))
}
}
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [NewsEditComponent], declarations: [NewsEditComponent],
providers: [ providers: [
{provide: AdminCommService, useValue: acMock} {provide: AdminCommService, useValue: acMock}
], ],
imports: [MatDialogModule, MatSnackBarModule] imports: [MatDialogModule, MatSnackBarModule, MatCardModule]
}); });
fixture = TestBed.createComponent(NewsEditComponent); fixture = TestBed.createComponent(NewsEditComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@@ -1,11 +1,10 @@
<!-- TODO: Remake the notifications module -->
<form [formGroup]="form" (ngSubmit)="submit()"> <form [formGroup]="form" (ngSubmit)="submit()">
<div formGroupName="recp"> <div formGroupName="recp">
<mat-radio-group formControlName="type"> <mat-radio-group formControlName="type">
<mat-radio-button value="uname"> <mat-radio-button value="uid">
<mat-form-field> <mat-form-field>
<mat-label>Nazwa użytkownika</mat-label> <mat-label>Użytkownik</mat-label>
<input matInput type="text" formControlName="uname"> <app-user-search formControlName="uid" required/>
</mat-form-field> </mat-form-field>
</mat-radio-button> </mat-radio-button>
<mat-radio-button value="room"> <mat-radio-button value="room">

View File

@@ -1,14 +1,76 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NotificationsComponent } from './notifications.component'; import { NotificationsComponent } from './notifications.component';
import { AdminCommService } from '../admin-comm.service';
import { RouterModule } from '@angular/router';
import { MatRadioModule } from '@angular/material/radio';
import { MatFormFieldControl, MatFormFieldModule } from '@angular/material/form-field';
import { Component, forwardRef } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { Observable, of } from 'rxjs';
import { AbstractControlDirective, ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, NgControl, ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
@Component({
selector: "app-user-search", template: '', providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => UserSearchStub),
multi: true,
},
{
provide: MatFormFieldControl,
useExisting: UserSearchStub
}]
})
class UserSearchStub implements ControlValueAccessor, MatFormFieldControl<never> {
value: null = null;
stateChanges: Observable<void> = of();
id: string = "";
placeholder: string = "";
ngControl: NgControl | AbstractControlDirective | null = null;
focused: boolean = false;
empty: boolean = true;
shouldLabelFloat: boolean = true;
required: boolean = false;
disabled: boolean = false;
errorState: boolean = false;
controlType?: string | undefined;
autofilled?: boolean | undefined;
userAriaDescribedBy?: string | undefined;
setDescribedByIds(ids: string[]): void {}
onContainerClick(event: MouseEvent): void {}
writeValue(obj: any): void {}
registerOnChange(fn: any): void {}
registerOnTouched(fn: any): void {}
setDisabledState?(isDisabled: boolean): void {}
}
describe('NotificationsComponent', () => { describe('NotificationsComponent', () => {
let component: NotificationsComponent; let component: NotificationsComponent;
let fixture: ComponentFixture<NotificationsComponent>; let fixture: ComponentFixture<NotificationsComponent>;
beforeEach(() => { beforeEach(() => {
const acMock = {
notif: {
getGroups: jasmine.createSpy("getGroups").and.returnValue(of())
}
}
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [NotificationsComponent] declarations: [NotificationsComponent, UserSearchStub],
providers: [
{provide: AdminCommService, useValue: acMock}
],
imports: [
RouterModule.forRoot([]),
MatRadioModule,
MatFormFieldModule,
MatIconModule,
FormsModule,
ReactiveFormsModule,
MatInputModule,
NoopAnimationsModule
]
}); });
fixture = TestBed.createComponent(NotificationsComponent); fixture = TestBed.createComponent(NotificationsComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@@ -1,20 +1,42 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms'; import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
import { AdminCommService } from '../admin-comm.service'; import { AdminCommService } from '../admin-comm.service';
import { Notification } from 'src/app/types/notification'; import { Notification } from 'src/app/types/notification';
import { Group } from 'src/app/types/group'; import { Group } from 'src/app/types/group';
import { LocalStorageService } from 'src/app/services/local-storage.service'; import { LocalStorageService } from 'src/app/services/local-storage.service';
import { ToolbarService } from '../toolbar/toolbar.service';
import { ActivatedRoute, Router } from '@angular/router';
import { UserSearchResult } from 'src/app/commonComponents/user-search/user-search.component';
@Component({ @Component({
selector: 'app-notifications', selector: 'app-notifications',
templateUrl: './notifications.component.html', templateUrl: './notifications.component.html',
styleUrls: ['./notifications.component.scss'] styleUrls: ['./notifications.component.scss']
}) })
export class NotificationsComponent implements OnInit { export class NotificationsComponent implements OnInit, OnDestroy {
groups!: Group[] groups!: Group[]
form = this.fb.group({
recp: this.fb.group({
uid: this.fb.control<UserSearchResult | null>(null),
room: this.fb.control<string|null>(null),
group: this.fb.control<string>(''),
type: this.fb.control<"room" | "uname" | "group">('uname', {nonNullable: true})
}),
title: this.fb.control('', {nonNullable: true}),
body: this.fb.control('', {nonNullable: true})
})
constructor (private readonly acs: AdminCommService, readonly ls: LocalStorageService) { } constructor (private readonly acs: AdminCommService, readonly ls: LocalStorageService, private toolbar: ToolbarService, private router: Router, private route: ActivatedRoute, private fb: FormBuilder ) {
this.toolbar.comp = this
this.toolbar.menu = [
{ title: "Wysłane", fn: "outbox", icon: "outbox" }
]
}
outbox() {
this.router.navigate(["outbox"], { relativeTo: this.route })
}
ngOnInit(): void { ngOnInit(): void {
this.acs.notif.getGroups().subscribe((v) => { this.acs.notif.getGroups().subscribe((v) => {
@@ -22,33 +44,21 @@ export class NotificationsComponent implements OnInit {
}) })
} }
ngOnDestroy(): void {
this.toolbar.comp = undefined
this.toolbar.menu = undefined
}
public inbox() {
}
success?: { sent: number; possible: number; }; success?: { sent: number; possible: number; };
form = new FormGroup<NotificationForm>({
recp: new FormGroup({
uname: new FormControl<string>(''),
room: new FormControl<string|null>(null),
group: new FormControl<string>(''),
type: new FormControl<"room" | "uname" | "group">('uname', {nonNullable: true})
}),
title: new FormControl('', {nonNullable: true}),
body: new FormControl('', {nonNullable: true})
})
submit() { submit() {
this.acs.notif.send(this.form.value as Notification).subscribe((data) => { this.acs.notif.send({...this.form.value, recp: {...this.form.get("recp")?.value, uid: this.form.controls['recp'].controls['uid'].value?._id}} as Notification).subscribe((data) => {
this.success = data this.success = data
}) })
} }
} }
interface NotificationForm {
body: FormControl<string>;
title: FormControl<string>;
recp: FormGroup<{
uname: FormControl<string | null>;
room: FormControl<string | null>;
group: FormControl<string | null>;
type: FormControl<"room" | "uname" | "group">;
}>
}

View File

@@ -0,0 +1,31 @@
<mat-card>
<mat-card-header>
<mat-card-title-group>
<mat-card-title>
{{item.title}}
</mat-card-title>
<mat-card-subtitle>{{item.sentDate.format('[Wysłano] dddd DD MMMM YYYYr. o HH:mm')}}</mat-card-subtitle>
</mat-card-title-group>
</mat-card-header>
<mat-card-content>
<p *ngIf="body">
{{body}}
</p>
<hr>
<ul>
@for (user of rcpts; track $index) {
<li>
<span *ngIf="user.room">{{user.room}}: </span>{{user.fname}} {{user.surname}} <span
style="color: gray">({{user.uname}})</span>
</li>
}
</ul>
</mat-card-content>
<mat-card-footer>
<mat-card-actions>
<button mat-stroked-button (click)="getMessage()" *ngIf="!body">Wczytaj treść</button>
<button mat-stroked-button (click)="getRcpts()" *ngIf="!rcpts">Wczytaj odbiorców</button>
<mat-spinner diameter="32" color="accent" *ngIf="loading"></mat-spinner>
</mat-card-actions>
</mat-card-footer>
</mat-card>

View File

@@ -0,0 +1,3 @@
mat-card-title {
font-size: 24pt;
}

View File

@@ -0,0 +1,36 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MessageComponent } from './message.component';
import { AdminCommService } from 'src/app/admin-view/admin-comm.service';
import { MatCardModule } from '@angular/material/card';
import * as moment from 'moment';
describe('MessageComponent', () => {
let component: MessageComponent;
let fixture: ComponentFixture<MessageComponent>;
beforeEach(async () => {
const acMock = {
}
await TestBed.configureTestingModule({
declarations: [MessageComponent],
providers: [
{provide: AdminCommService, useValue: acMock}
],
imports: [
MatCardModule
]
})
.compileComponents();
fixture = TestBed.createComponent(MessageComponent);
component = fixture.componentInstance;
component.item = {_id: "test", sentDate: moment(), title: "Test"}
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,31 @@
import { Component, Input } from '@angular/core';
import { AdminCommService } from 'src/app/admin-view/admin-comm.service';
@Component({
selector: 'app-message',
templateUrl: './message.component.html',
styleUrl: './message.component.scss'
})
export class MessageComponent {
@Input() item!: {_id: string, sentDate: moment.Moment, title: string}
body?: string
rcpts?: {_id: string, uname: string, room?: string, fname?: string, surname?: string}[]
loading: boolean = false
constructor (readonly acu: AdminCommService) {}
getMessage() {
this.loading = true
this.acu.notif.outbox.getBody(this.item._id).subscribe(v => {
this.body = v
this.loading = false
})
}
getRcpts() {
this.loading = true
this.acu.notif.outbox.getRcpts(this.item._id).subscribe(v => {
this.rcpts = v
this.loading = false
})
}
}

View File

@@ -0,0 +1,6 @@
<p>Wysłane wiadomości:</p>
<div class="cardContainer">
@for (item of messages; track $index) {
<app-message [item]="item"></app-message>
}
</div>

View File

@@ -0,0 +1,6 @@
.cardContainer {
display: flex;
flex-wrap: wrap;
gap: 1ch;
margin: 1ch;
}

View File

@@ -0,0 +1,39 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { OutboxComponent } from './outbox.component';
import { AdminCommService } from '../../admin-comm.service';
import { RouterModule } from '@angular/router';
import { of } from 'rxjs';
describe('OutboxComponent', () => {
let component: OutboxComponent;
let fixture: ComponentFixture<OutboxComponent>;
beforeEach(async () => {
const acMock = {
notif: {
outbox: {
getSent: jasmine.createSpy("getSent").and.returnValue(of())
}
}
}
await TestBed.configureTestingModule({
declarations: [OutboxComponent],
providers: [
{provide: AdminCommService, useValue: acMock}
],
imports: [
RouterModule.forRoot([])
]
})
.compileComponents();
fixture = TestBed.createComponent(OutboxComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,42 @@
import { Component, OnInit } from '@angular/core';
import { AdminCommService } from '../../admin-comm.service';
import { Router, ActivatedRoute } from '@angular/router';
import { ToolbarService } from '../../toolbar/toolbar.service';
import * as moment from 'moment';
@Component({
selector: 'app-outbox',
templateUrl: './outbox.component.html',
styleUrl: './outbox.component.scss'
})
export class OutboxComponent implements OnInit {
messages!: {
_id: string;
sentDate: moment.Moment;
title: string;
}[]
constructor (private readonly acs: AdminCommService, private toolbar: ToolbarService, private router: Router, private route: ActivatedRoute ) {
this.toolbar.comp = this
this.toolbar.menu = [
{ title: "Powiadomienia", fn: "goBack", icon: "arrow_back" }
]
}
goBack() {
this.router.navigate(['../'], {relativeTo: this.route})
}
ngOnInit(): void {
this.acs.notif.outbox.getSent().subscribe((v) => {
this.messages = v.map(i => {
return {
...i,
sentDate: moment(i.sentDate)
}
})
})
}
}

View File

@@ -1,4 +1,5 @@
<mat-accordion> <mat-accordion>
<!-- #region Rooms-->
<mat-expansion-panel> <mat-expansion-panel>
<!-- TODO: Make more ergonomic --> <!-- TODO: Make more ergonomic -->
<mat-expansion-panel-header> <mat-expansion-panel-header>
@@ -8,6 +9,8 @@
<p>Kliknij listę aby edytować</p> <p>Kliknij listę aby edytować</p>
<app-list-editor [converter]="usettings.rooms" (edit)="saveRoom($event)"></app-list-editor> <app-list-editor [converter]="usettings.rooms" (edit)="saveRoom($event)"></app-list-editor>
</mat-expansion-panel> </mat-expansion-panel>
<!-- #endregion -->
<!-- #region Room grade reasons-->
<mat-expansion-panel> <mat-expansion-panel>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title>Powody nieczystości</mat-panel-title> <mat-panel-title>Powody nieczystości</mat-panel-title>
@@ -16,6 +19,8 @@
<p>Kliknij listę aby edytować</p> <p>Kliknij listę aby edytować</p>
<app-list-editor [list]="usettings.cleanThings" (edit)="saveCleanThings($event)"></app-list-editor> <app-list-editor [list]="usettings.cleanThings" (edit)="saveCleanThings($event)"></app-list-editor>
</mat-expansion-panel> </mat-expansion-panel>
<!-- #endregion -->
<!-- #region Key rooms-->
<mat-expansion-panel> <mat-expansion-panel>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title>Sale z kluczami</mat-panel-title> <mat-panel-title>Sale z kluczami</mat-panel-title>
@@ -23,6 +28,68 @@
</mat-expansion-panel-header> </mat-expansion-panel-header>
<app-list-editor [list]="usettings.keyrooms" (edit)="saveKeyrooms($event)"></app-list-editor> <app-list-editor [list]="usettings.keyrooms" (edit)="saveKeyrooms($event)"></app-list-editor>
</mat-expansion-panel> </mat-expansion-panel>
<!-- #endregion -->
<!-- #region Default menu items-->
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Domyślne wpisy jadłospisu</mat-panel-title>
<mat-panel-description></mat-panel-description>
</mat-expansion-panel-header>
<table>
<caption>Domyślne wpisy w jadłospisie dla danych pozycji</caption>
<thead>
<tr>
<th>Śniadanie</th>
<th>Kolacja</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<app-list-editor [list]="usettings.menu.defaultItems.sn" (edit)="saveSn($event)"/>
</td>
<td>
<app-list-editor [list]="usettings.menu.defaultItems.kol" (edit)="saveKol($event)"/>
</td>
</tr>
</tbody>
</table>
</mat-expansion-panel>
<!-- #endregion -->
<!-- #region Security-->
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Bezpieczeństwo</mat-panel-title>
</mat-expansion-panel-header>
<mat-tab-group color="accent">
<mat-tab label="Konta">
<p>
Domyślne hasło użytkownika po wygenerowaniu konto to <code>pierwszelogowanie</code><br>
Reset hasła powoduje zmianę na <code>reset</code>
</p>
<form [formGroup]="accSec" (submit)="saveAccSecTimeouts()">
<p>Ograniczenia logowania</p>
<mat-form-field color="accent">
<mat-label>Dozwolone próby logowania</mat-label>
<input matInput type="number" formControlName="attempts">
</mat-form-field><br>
<mat-form-field color="accent">
<mat-label>Okres liczenia prób</mat-label>
<input matInput type="number" formControlName="time">
<mat-hint>Podaj w minutach</mat-hint>
</mat-form-field><br>
<mat-form-field color="accent">
<mat-label>Czas blokady konta</mat-label>
<input matInput type="number" formControlName="lockout">
<mat-hint>Podaj w minutach</mat-hint>
</mat-form-field><br>
<button mat-flat-button color="accent">Zapisz</button>
</form>
</mat-tab>
</mat-tab-group>
</mat-expansion-panel>
<!-- #endregion -->
<!-- #region Program control-->
<mat-expansion-panel> <mat-expansion-panel>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title>Sterowanie programem</mat-panel-title> <mat-panel-title>Sterowanie programem</mat-panel-title>
@@ -41,4 +108,5 @@
Wyloguj wszystkich użytkowników Wyloguj wszystkich użytkowników
</button> --> </button> -->
</mat-expansion-panel> </mat-expansion-panel>
<!-- #endregion -->
</mat-accordion> </mat-accordion>

View File

@@ -1,14 +1,49 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SettingsComponent } from './settings.component'; import { SettingsComponent } from './settings.component';
import { AdminCommService } from '../admin-comm.service';
import { MatExpansionModule } from '@angular/material/expansion';
import { Component, Input } from '@angular/core';
import { MatTabsModule } from '@angular/material/tabs';
import { MatFormFieldModule } from '@angular/material/form-field';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { of } from 'rxjs';
import { MatInputModule } from '@angular/material/input';
@Component({selector: 'app-list-editor', template: ''})
class ListEditorStub {
@Input() converter?: any[];
@Input() list?: string[];
}
describe('SettingsComponent', () => { describe('SettingsComponent', () => {
let component: SettingsComponent; let component: SettingsComponent;
let fixture: ComponentFixture<SettingsComponent>; let fixture: ComponentFixture<SettingsComponent>;
beforeEach(async () => { beforeEach(async () => {
const acMock = {
settings: {
getAll: jasmine.createSpy("getAll").and.returnValue(of())
}
}
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [SettingsComponent] declarations: [SettingsComponent, ListEditorStub],
providers: [
{provide: AdminCommService, useValue: acMock}
],
imports: [
MatExpansionModule,
MatTabsModule,
MatFormFieldModule,
FormsModule,
ReactiveFormsModule,
MatIconModule,
NoopAnimationsModule,
MatInputModule
]
}) })
.compileComponents(); .compileComponents();

View File

@@ -1,6 +1,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { AdminCommService } from '../admin-comm.service'; import { AdminCommService } from '../admin-comm.service';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { FormBuilder } from '@angular/forms';
@Component({ @Component({
selector: 'app-settings', selector: 'app-settings',
@@ -8,13 +9,21 @@ import { MatSnackBar } from '@angular/material/snack-bar';
styleUrl: './settings.component.scss' styleUrl: './settings.component.scss'
}) })
export class SettingsComponent implements OnInit { export class SettingsComponent implements OnInit {
usettings!: IUSettings usettings: IUSettings = {cleanThings: [], keyrooms: [], menu: {defaultItems: {kol: [], sn: []}}, rooms: [], security: {loginTimeout: {attempts: 0, lockout: 0, time: 0}}}
reloadTimeout: boolean = false; reloadTimeout: boolean = false;
constructor (private readonly acu: AdminCommService, private readonly sb: MatSnackBar) { } constructor (private readonly acu: AdminCommService, private readonly sb: MatSnackBar, private readonly fb: FormBuilder) { }
accSec = this.fb.nonNullable.group({
attempts: this.fb.nonNullable.control(1),
time: this.fb.nonNullable.control(1),
lockout: this.fb.nonNullable.control(1),
})
ngOnInit(): void { ngOnInit(): void {
this.acu.settings.getAll().subscribe((r) => { this.acu.settings.getAll().subscribe((r) => {
this.usettings = r this.usettings = r
this.accSecTimeouts = r.security.loginTimeout
}) })
} }
@@ -31,10 +40,39 @@ export class SettingsComponent implements OnInit {
this.send() this.send()
} }
saveSn(event: string[]) {
this.usettings.menu.defaultItems.sn = event
this.send()
}
saveKol(event: string[]) {
this.usettings.menu.defaultItems.kol = event
this.send()
}
saveAccSecTimeouts() {
this.usettings.security.loginTimeout = this.accSecTimeouts
this.send()
}
set accSecTimeouts(value: IUSettings['security']['loginTimeout']) {
this.accSec.setValue({
attempts: value.attempts,
lockout: value.lockout / 60,
time: value.time / 60
})
}
get accSecTimeouts(): IUSettings['security']['loginTimeout'] {
return {
attempts: this.accSec.controls['attempts'].value,
lockout: this.accSec.controls['lockout'].value * 60,
time: this.accSec.controls['time'].value * 60
}
}
send() { send() {
this.acu.settings.post(this.usettings).subscribe((s) => { this.acu.settings.post(this.usettings).subscribe((s) => {
if (s.status == 200) { if (s.status == 200) {
this.sb.open("Zapisano!", undefined, {duration: 1000}) this.sb.open("Zapisano!", undefined, { duration: 1000 })
} else { } else {
console.error(s); console.error(s);
} }
@@ -51,7 +89,7 @@ export class SettingsComponent implements OnInit {
}, 5000); }, 5000);
this.acu.settings.reload().subscribe((s) => { this.acu.settings.reload().subscribe((s) => {
if (s.status == 200) { if (s.status == 200) {
this.sb.open("Przeładowano ustawienia!", undefined, {duration: 3000}) this.sb.open("Przeładowano ustawienia!", undefined, { duration: 3000 })
} else { } else {
console.error(s); console.error(s);
} }
@@ -63,4 +101,17 @@ export interface IUSettings {
keyrooms: string[]; keyrooms: string[];
rooms: string[]; rooms: string[];
cleanThings: string[]; cleanThings: string[];
menu: {
defaultItems: {
sn: string[];
kol: string[];
}
};
security: {
loginTimeout: {
attempts: number;
time: number;
lockout: number;
}
}
} }

View File

@@ -0,0 +1 @@
<span class="main"><mat-icon class="icon">arrow_upward</mat-icon><span>Wybierz zakładkę w menu</span></span>

View File

@@ -0,0 +1,13 @@
.main {
margin-top: 8px;
margin-left: 16px;
display: flex;
align-items: center;
gap: 1ch;
}
.icon {
width: fit-content;
height: fit-content;
font-size: 32pt;
}

View File

@@ -0,0 +1,27 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { StartAdminComponent } from './start.component';
import { MatIconModule } from '@angular/material/icon';
describe('StartAdminComponent', () => {
let component: StartAdminComponent;
let fixture: ComponentFixture<StartAdminComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [StartAdminComponent],
imports: [
MatIconModule
]
})
.compileComponents();
fixture = TestBed.createComponent(StartAdminComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-start',
templateUrl: './start.component.html',
styleUrl: './start.component.scss'
})
export class StartAdminComponent {
}

View File

@@ -0,0 +1,14 @@
<mat-toolbar color="accent">
<button mat-icon-button (click)="drawer.toggle()"><mat-icon>menu</mat-icon></button>
<span>{{title.getTitle()}}</span>
<span style="flex: 1 1 auto"></span>
<button mat-icon-button *ngIf="toolbar.menu" [matMenuTriggerFor]="menu" (click)="openMenu()"><mat-icon>more_vert</mat-icon></button>
</mat-toolbar>
<mat-menu #menu="matMenu">
@for (item of _menu; track $index) {
<button mat-menu-item *ngIf="item.check ?? true" (click)="toolbar.comp[item.fn]()">
<mat-icon *ngIf="item.icon">{{item.icon}}</mat-icon>
<span>{{item.title}}</span>
</button>
}
</mat-menu>

View File

@@ -0,0 +1,27 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ToolbarComponent } from './toolbar.component';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
describe('ToolbarComponent', () => {
let component: ToolbarComponent;
let fixture: ComponentFixture<ToolbarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ToolbarComponent],
imports: [MatToolbarModule, MatIconModule, MatMenuModule]
})
.compileComponents();
fixture = TestBed.createComponent(ToolbarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,26 @@
import { Component, Input, ViewChild } from '@angular/core';
import { MatDrawer } from '@angular/material/sidenav';
import { Title } from '@angular/platform-browser';
import { ToolbarService } from './toolbar.service';
import { MatMenuTrigger } from '@angular/material/menu';
@Component({
selector: 'app-toolbar',
templateUrl: './toolbar.component.html',
styleUrl: './toolbar.component.scss'
})
export class ToolbarComponent {
@Input() drawer!: MatDrawer;
@ViewChild(MatMenuTrigger) trigger!: MatMenuTrigger;
protected _menu?: typeof this.toolbar.menu
constructor(readonly title: Title, protected toolbar: ToolbarService) {
}
openMenu () {
this._menu = this.toolbar.menu
this.trigger.openMenu()
}
}

View File

@@ -6,9 +6,6 @@ import { Injectable } from '@angular/core';
export class ToolbarService { export class ToolbarService {
public comp?: any; public comp?: any;
public menu?: {title: string, check: boolean, icon?: string, fn: string}[] public menu?: {title: string, check?: boolean, icon?: string, fn: string}[]
constructor() { }
} }

View File

@@ -4,6 +4,6 @@ import { LocalStorageService } from './services/local-storage.service';
export const adminGuard: CanActivateChildFn = (childRoute, state) => { export const adminGuard: CanActivateChildFn = (childRoute, state) => {
const router = inject(Router) const router = inject(Router)
if (inject(LocalStorageService).admin == false) return router.parseUrl('/') if (inject(LocalStorageService).admin == undefined) return router.parseUrl('/')
return true return true
}; };

View File

@@ -19,6 +19,8 @@ import { SummaryComponent } from './admin-view/grades/summary/summary.component'
import { SettingsComponent } from './admin-view/settings/settings.component'; import { SettingsComponent } from './admin-view/settings/settings.component';
import { AttendenceSummaryComponent } from './admin-view/grades/attendence-summary/attendence-summary.component'; import { AttendenceSummaryComponent } from './admin-view/grades/attendence-summary/attendence-summary.component';
import { NotificationsComponent } from './admin-view/notifications/notifications.component'; import { NotificationsComponent } from './admin-view/notifications/notifications.component';
import { OutboxComponent } from './admin-view/notifications/outbox/outbox.component';
import { StartAdminComponent } from './admin-view/start/start.component';
const routes: Routes = [ const routes: Routes = [
{path: "", redirectTo: "login", pathMatch: "full"}, {path: "", redirectTo: "login", pathMatch: "full"},
@@ -30,10 +32,14 @@ const routes: Routes = [
{path: "grades", component: PersonalComponent, title: "Konto"} {path: "grades", component: PersonalComponent, title: "Konto"}
]}, ]},
{path: "admin", component: AdminViewComponent, title: "Panel administracyjny", canActivateChild: [authGuard, adminGuard], children: [ {path: "admin", component: AdminViewComponent, title: "Panel administracyjny", canActivateChild: [authGuard, adminGuard], children: [
{path: "", pathMatch: "full", component: StartAdminComponent},
{path: "news", title: "Edytowanie wiadomości", component: NewsEditComponent}, {path: "news", title: "Edytowanie wiadomości", component: NewsEditComponent},
{path: "menu", title: "Edytowanie jadłospisu", component: MenuNewComponent}, {path: "menu", title: "Edytowanie jadłospisu", component: MenuNewComponent},
{path: "accounts", title: "Użytkownicy", component: AccountMgmtComponent}, {path: "accounts", title: "Użytkownicy", component: AccountMgmtComponent},
{path: "notifications", title: "Powiadomienia", component: NotificationsComponent}, {path: "notifications", children: [
{path: "", pathMatch: "full", title: "Powiadomienia", component: NotificationsComponent},
{path: "outbox", title: "Wysłane", component: OutboxComponent}
]},
{path: "groups", title: "Grupy", component: GroupsComponent}, {path: "groups", title: "Grupy", component: GroupsComponent},
{path: "keys", title: "Klucze", component: AdminKeyComponent}, {path: "keys", title: "Klucze", component: AdminKeyComponent},
{path: "grades", children: [ {path: "grades", children: [

View File

@@ -16,6 +16,9 @@
:host { :host {
width: 100%; width: 100%;
@supports (-webkit-touch-callout: none) {
height: 95vh;
}
height: 100vh; height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -12,22 +12,24 @@ import { of } from 'rxjs';
describe('AppViewComponent', () => { describe('AppViewComponent', () => {
let component: AppViewComponent; let component: AppViewComponent;
let fixture: ComponentFixture<AppViewComponent>; let fixture: ComponentFixture<AppViewComponent>;
let authClient: jasmine.SpyObj<AuthClient>;
beforeEach(() => { beforeEach(() => {
const authSpy = jasmine.createSpyObj('AuthClient', ['check']) const authSpy = jasmine.createSpyObj('AuthClient', ['check'])
const pushSpy = jasmine.createSpyObj('SwPush', ['requestSubscription']) const pushSpy = jasmine.createSpyObj('SwPush', ['requestSubscription'])
const updatesSpy = jasmine.createSpyObj('UpdatesService', ['postNotif']) const updatesSpy = jasmine.createSpyObj('UpdatesService', {
newsCheck: of()
})
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [AppViewComponent], declarations: [AppViewComponent],
providers: [{provide: AuthClient, useValue: authSpy}, providers: [
{provide: SwPush, useValue: pushSpy}, {provide: AuthClient, useValue: authSpy},
{provide: UpdatesService, useValue: updatesSpy}], {provide: SwPush, useValue: pushSpy},
{provide: UpdatesService, useValue: updatesSpy}
],
imports: [MatTabsModule, RouterModule.forRoot([]), MatIconModule] imports: [MatTabsModule, RouterModule.forRoot([]), MatIconModule]
}); });
fixture = TestBed.createComponent(AppViewComponent); fixture = TestBed.createComponent(AppViewComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
authClient = TestBed.inject(AuthClient) as jasmine.SpyObj<AuthClient>
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@@ -6,6 +6,8 @@ import { Link } from '../types/link';
import { LocalStorageService } from '../services/local-storage.service'; import { LocalStorageService } from '../services/local-storage.service';
import { interval } from 'rxjs'; import { interval } from 'rxjs';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { MatDialog } from '@angular/material/dialog';
import { NotifDialogComponent } from './notif-dialog/notif-dialog.component';
@Component({ @Component({
selector: 'app-app-view', selector: 'app-app-view',
@@ -25,7 +27,14 @@ export class AppViewComponent implements OnInit {
}); });
} }
constructor (private ac: AuthClient, readonly swPush: SwPush, private us: UpdatesService, private ls: LocalStorageService, private sb: MatSnackBar) {} constructor (
private ac: AuthClient,
readonly swPush: SwPush,
private us: UpdatesService,
private ls: LocalStorageService,
private sb: MatSnackBar,
private dialog: MatDialog
) {}
subscribeToNotif() { subscribeToNotif() {
if (this.swPush.isEnabled && this.ls.capCheck(4)) { if (this.swPush.isEnabled && this.ls.capCheck(4)) {
@@ -45,6 +54,13 @@ export class AppViewComponent implements OnInit {
} }
newsCheck() { newsCheck() {
if (this.ls.capCheck(4)) {
this.us.getNotifCheck().subscribe((s) => {
s.forEach(v => {
this.dialog.open(NotifDialogComponent, {data: v})
})
})
}
if (this.ls.newsflag) return; if (this.ls.newsflag) return;
this.us.newsCheck().subscribe((s) => { this.us.newsCheck().subscribe((s) => {
if (s.hash != this.ls.newsCheck.hash) { if (s.hash != this.ls.newsCheck.hash) {

View File

@@ -13,8 +13,8 @@
<mat-card-content> <mat-card-content>
<ul> <ul>
<li *ngFor="let i of ls.defaultItems.sn">{{i}}</li> <li *ngFor="let i of ls.defaultItems.sn">{{i}}</li>
<li *ngFor="let i of getsn.fancy">{{i.charAt(0).toUpperCase()+i.substring(1)}}</li> <li *ngFor="let i of getsn.fancy">{{capitalize(i)}}</li>
<li *ngIf="getsn.second">{{getsn.second.charAt(0).toUpperCase()+getsn.second.substring(1)}}</li> <li *ngIf="getsn.second">{{capitalize(getsn.second)}}</li>
</ul> </ul>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
@@ -51,7 +51,7 @@
<button mat-icon-button (click)="vote('kol', '-')"><mat-icon [color]="menu!.kolv == '-' ? 'warn' : null">thumb_down</mat-icon></button> <button mat-icon-button (click)="vote('kol', '-')"><mat-icon [color]="menu!.kolv == '-' ? 'warn' : null">thumb_down</mat-icon></button>
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>
<mat-card *ngIf="!(getkol || getob || getsn || loading)"> <mat-card *ngIf="!(getkol || getob || getsn || loading || gettitle)">
<mat-card-content id="no-data"> <mat-card-content id="no-data">
Brak danych, wybierz inny dzień. Brak danych, wybierz inny dzień.
</mat-card-content> </mat-card-content>

View File

@@ -14,13 +14,17 @@ import { MatInputModule } from '@angular/material/input';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatDialogRef } from '@angular/material/dialog'; import { MatDialogRef } from '@angular/material/dialog';
import { MatBottomSheet, MatBottomSheetModule } from '@angular/material/bottom-sheet'; import { MatBottomSheet, MatBottomSheetModule } from '@angular/material/bottom-sheet';
import { of } from 'rxjs';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
describe('MenuComponent', () => { describe('MenuComponent', () => {
let component: MenuComponent; let component: MenuComponent;
let fixture: ComponentFixture<MenuComponent>; let fixture: ComponentFixture<MenuComponent>;
beforeEach(async () => { beforeEach(async () => {
const updatesSpy = jasmine.createSpyObj('UpdatesService', ['getMenu']) const updatesSpy = jasmine.createSpyObj('UpdatesService', {
getMenu: of()
})
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ MenuComponent, DateSelectorComponent], declarations: [ MenuComponent, DateSelectorComponent],
providers: [ providers: [
@@ -30,7 +34,17 @@ describe('MenuComponent', () => {
{provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS}, {provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS},
{provide: MAT_MOMENT_DATE_ADAPTER_OPTIONS, useValue: {useUtc: true}}, {provide: MAT_MOMENT_DATE_ADAPTER_OPTIONS, useValue: {useUtc: true}},
], ],
imports: [MatIconModule, MatFormFieldModule, MatDatepickerModule, MatCardModule, ReactiveFormsModule, MatInputModule, BrowserAnimationsModule, MatBottomSheetModule] imports: [
MatIconModule,
MatFormFieldModule,
MatDatepickerModule,
MatCardModule,
ReactiveFormsModule,
MatInputModule,
BrowserAnimationsModule,
MatBottomSheetModule,
MatProgressSpinnerModule
]
}) })
.compileComponents(); .compileComponents();

View File

@@ -32,17 +32,34 @@ export class MenuComponent {
} }
menu?: Menu; menu?: Menu;
get getsn() {return (this.menu && this.menu.sn) ? this.menu.sn : null} get getsn() {return (this.menu && this.checkIfAnyProperty(this.menu.sn)) ? this.menu.sn : null}
get getob() {return (this.menu && this.menu.ob) ? this.menu.ob : null} get getob() {return (this.menu && this.checkIfAnyProperty(this.menu.ob)) ? this.menu.ob : null}
get getkol() {return (this.menu && this.menu.kol) ? this.menu.kol : null} get getkol() {return (this.menu && this.menu.kol) ? this.menu.kol : null}
get gettitle() {return (this.menu && this.menu.dayTitle && this.menu.dayTitle != "") ? this.menu.dayTitle : null} get gettitle() {return (this.menu && this.menu.dayTitle && this.menu.dayTitle != "") ? this.menu.dayTitle : null}
private checkIfAnyProperty(obj: { [x: string]: string | string[];}) {
for (let i in obj) {
if (Array.isArray(obj[i])) {
if (obj[i].length > 0) return true
} else {
if (!!obj[i]) return true
}
}
return false
}
capitalize(str: string) {
return str.charAt(0).toUpperCase()+str.substring(1)
}
updateMenu(silent?: boolean) { updateMenu(silent?: boolean) {
this.loading = !silent this.loading = !silent
if (!silent) this.menu = undefined if (!silent) this.menu = undefined
this.uc.getMenu(this.day).subscribe(m => { this.uc.getMenu(this.day).subscribe(m => {
this.loading = false this.loading = false
this.menu = m this.menu = m
console.log(m);
}) })
} }

View File

@@ -9,4 +9,9 @@
<mat-card-footer> <mat-card-footer>
<p>{{item.date | date:'d-LL-yyyy HH:mm'}}</p> <p>{{item.date | date:'d-LL-yyyy HH:mm'}}</p>
</mat-card-footer> </mat-card-footer>
</mat-card>
<mat-card *ngIf="news.length == 0">
<p>
Brak wiadomości.
</p>
</mat-card> </mat-card>

View File

@@ -29,3 +29,7 @@ mat-card-footer p {
mat-card-content p { mat-card-content p {
white-space: pre-line; white-space: pre-line;
} }
mat-card p {
margin: 15px;
}

View File

@@ -3,6 +3,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NewsComponent } from './news.component'; import { NewsComponent } from './news.component';
import { UpdatesService } from 'src/app/services/updates.service'; import { UpdatesService } from 'src/app/services/updates.service';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { LocalStorageService } from 'src/app/services/local-storage.service';
import { MatCardModule } from '@angular/material/card';
describe('NewsComponent', () => { describe('NewsComponent', () => {
let component: NewsComponent; let component: NewsComponent;
@@ -12,11 +16,20 @@ describe('NewsComponent', () => {
const updatesMock = jasmine.createSpyObj('UpdatesService', { const updatesMock = jasmine.createSpyObj('UpdatesService', {
getNews: of() getNews: of()
}) })
const lsMock = {
news: []
}
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ NewsComponent ], declarations: [ NewsComponent ],
providers: [ providers: [
{provide: UpdatesService, useValue: updatesMock} {provide: UpdatesService, useValue: updatesMock},
{provide: LocalStorageService, useValue: lsMock}
], ],
imports: [
MatProgressSpinnerModule,
NoopAnimationsModule,
MatCardModule
]
}) })
.compileComponents(); .compileComponents();

View File

@@ -0,0 +1,10 @@
<h1 mat-dialog-title>{{data.message.title}}</h1>
<mat-dialog-content>
<p>
{{data.message.body}}
</p>
<div>{{data.sentDate.format("[Wysłano] dddd DD MMMM YYYYr. o HH:mm")}}</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-raised-button color="primary" (click)="ack()">Odczytano</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,37 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NotifDialogComponent } from './notif-dialog.component';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { UpdatesService } from 'src/app/services/updates.service';
import { of } from 'rxjs';
describe('NotifDialogComponent', () => {
let component: NotifDialogComponent;
let fixture: ComponentFixture<NotifDialogComponent>;
beforeEach(async () => {
const uMock = jasmine.createSpyObj<UpdatesService>("UpdatesService", {
postInfoAck: of()
})
await TestBed.configureTestingModule({
declarations: [NotifDialogComponent],
providers: [
{provide: MAT_DIALOG_DATA, useValue: {message: "Test"}},
{provide: MatDialogRef, useValue: {}},
{provide: UpdatesService, useValue: uMock}
],
imports: [
MatDialogModule
]
})
.compileComponents();
fixture = TestBed.createComponent(NotifDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,27 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import * as moment from 'moment';
import { UpdatesService } from 'src/app/services/updates.service';
@Component({
selector: 'app-notif-dialog',
templateUrl: './notif-dialog.component.html',
styleUrl: './notif-dialog.component.scss'
})
export class NotifDialogComponent {
constructor (
@Inject(MAT_DIALOG_DATA) public data: {_id: string, message: {title: string, body: string}, sentDate: moment.Moment},
public dialogRef: MatDialogRef<NotifDialogComponent>,
private uc: UpdatesService
) {
data.sentDate = moment(data.sentDate)
}
ack () {
this.uc.postInfoAck(this.data._id).subscribe((v) => {
this.dialogRef.close()
})
}
}

View File

@@ -1,6 +1,8 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AboutComponent } from './about.component'; import { AboutComponent } from './about.component';
import { MatDialogModule } from '@angular/material/dialog';
import { MatListModule } from '@angular/material/list';
describe('AboutComponent', () => { describe('AboutComponent', () => {
let component: AboutComponent; let component: AboutComponent;
@@ -8,7 +10,11 @@ describe('AboutComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [AboutComponent] declarations: [AboutComponent],
imports: [
MatDialogModule,
MatListModule
]
}) })
.compileComponents(); .compileComponents();

View File

@@ -1,14 +1,37 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CleanComponent } from './clean.component'; import { CleanComponent } from './clean.component';
import { UpdatesService } from 'src/app/services/updates.service';
import { of } from 'rxjs';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatDatepicker } from '@angular/material/datepicker';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import * as moment from 'moment';
@Component({selector: "app-date-selector", template: ''})
class DateSelectorStub {
@Input() date: moment.Moment = moment.utc().startOf('day');
@Output() dateChange = new EventEmitter<moment.Moment>();
@Input() filter: (date: moment.Moment | null) => boolean = () => true
}
describe('CleanComponent', () => { describe('CleanComponent', () => {
let component: CleanComponent; let component: CleanComponent;
let fixture: ComponentFixture<CleanComponent>; let fixture: ComponentFixture<CleanComponent>;
let updates: jasmine.SpyObj<UpdatesService>
beforeEach(async () => { beforeEach(async () => {
updates = jasmine.createSpyObj<UpdatesService>("UpdatesService", {
getClean: of()
})
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [CleanComponent] declarations: [CleanComponent, DateSelectorStub],
providers: [
{provide: UpdatesService, useValue: updates}
],
imports: [MatDialogModule, MatIconModule, MatFormFieldModule, MatDatepicker]
}) })
.compileComponents(); .compileComponents();

View File

@@ -0,0 +1,14 @@
<h1 mat-dialog-title>Dodatkowe ustawienia</h1>
<mat-dialog-content>
<mat-action-list>
@for (link of LINKS; track link) {
<button mat-list-item (click)="open(link.component)">
<mat-icon matListItemIcon *ngIf="link.icon">{{link.icon}}</mat-icon>
<div matListItemTitle>{{link.title}}</div>
</button>
}
</mat-action-list>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-dialog-close mat-button>Zamknij</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,29 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ExtraComponent } from './extra.component';
import { MatDialogModule } from '@angular/material/dialog';
import { MatListModule } from '@angular/material/list';
describe('ExtraComponent', () => {
let component: ExtraComponent;
let fixture: ComponentFixture<ExtraComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ExtraComponent],
imports: [
MatDialogModule,
MatListModule
]
})
.compileComponents();
fixture = TestBed.createComponent(ExtraComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,29 @@
import { ComponentType } from '@angular/cdk/portal';
import { Component } from '@angular/core';
import { Link } from 'src/app/types/link';
import { RedirectComponent } from './redirect/redirect.component';
import { MatDialog } from '@angular/material/dialog';
@Component({
selector: 'app-extra',
templateUrl: './extra.component.html',
styleUrl: './extra.component.scss'
})
export class ExtraComponent {
constructor (private dialog: MatDialog) {}
private readonly _LINKS: (Omit<Link, "href"> & {component: ComponentType<any>})[] = [
{ title: "Domyślna strona po logowaniu", component: RedirectComponent, enabled: true, icon: "home" }
]
public get LINKS() {
return this._LINKS.filter((v) => {
return v.enabled
});
}
open(component: ComponentType<any>) {
this.dialog.open(component)
}
}

View File

@@ -0,0 +1,14 @@
<h1 mat-dialog-title>Domyślna strona po logowaniu</h1>
<mat-dialog-content>
<p>Wpisz link względem /ipwa/ w poniższym polu.</p>
<p>Przykład: /app/menu</p>
<p style="color: red">Jeśli nie wiesz co tu wpisać, najlepiej nie zmieniaj tego ustawienia</p>
<mat-form-field>
<input matInput type="text" [(ngModel)]="redirect">
<mat-label>Link</mat-label>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-dialog-close mat-button>Anuluj</button>
<button (click)="save()" mat-flat-button>Zapisz</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,40 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatInputHarness } from '@angular/material/input/testing'
import { RedirectComponent } from './redirect.component';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { AuthClient } from 'src/app/services/auth.client';
import { MatFormFieldModule } from '@angular/material/form-field';
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'
import { FormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('RedirectComponent', () => {
let component: RedirectComponent;
let fixture: ComponentFixture<RedirectComponent>;
let loader: HarnessLoader
let authMock
beforeEach(async () => {
authMock = jasmine.createSpyObj<AuthClient>("AuthClient", {}, {redirect: ''})
await TestBed.configureTestingModule({
declarations: [RedirectComponent],
providers: [
{provide: MatDialogRef, useValue: {}},
{provide: AuthClient, useValue: authMock}
],
imports: [MatDialogModule, MatFormFieldModule, MatInputModule, FormsModule, NoopAnimationsModule]
})
.compileComponents();
fixture = TestBed.createComponent(RedirectComponent);
component = fixture.componentInstance;
fixture.detectChanges();
loader = TestbedHarnessEnvironment.loader(fixture)
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,20 @@
import { Component } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
import { AuthClient } from 'src/app/services/auth.client';
@Component({
selector: 'app-redirect',
templateUrl: './redirect.component.html',
styleUrl: './redirect.component.scss'
})
export class RedirectComponent {
protected redirect = ""
constructor (public dialogRef: MatDialogRef<RedirectComponent>, private ac: AuthClient) {
this.redirect = ac.redirect
}
protected save() {
this.ac.redirect = this.redirect
this.dialogRef.close()
}
}

View File

@@ -1,14 +1,26 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { KeyComponent } from './key.component'; import { KeyComponent } from './key.component';
import { UpdatesService } from 'src/app/services/updates.service';
import { of } from 'rxjs';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
describe('KeyComponent', () => { describe('KeyComponent', () => {
let component: KeyComponent; let component: KeyComponent;
let fixture: ComponentFixture<KeyComponent>; let fixture: ComponentFixture<KeyComponent>;
let uMock: jasmine.SpyObj<UpdatesService>
beforeEach(async () => { beforeEach(async () => {
uMock = jasmine.createSpyObj<UpdatesService>("UpdatesService", {
getKeys: of()
})
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [KeyComponent] declarations: [KeyComponent],
providers: [
{provide: UpdatesService, useValue: uMock}
],
imports: [MatDialogModule, MatIconModule]
}) })
.compileComponents(); .compileComponents();

View File

@@ -30,6 +30,10 @@
<div matListItemTitle>Panel administracyjny</div> <div matListItemTitle>Panel administracyjny</div>
<div matListItemLine>Poprzednio Tryb edycji</div> <div matListItemLine>Poprzednio Tryb edycji</div>
</button> </button>
<button mat-list-item (click)="openExtra()">
<mat-icon matListItemIcon>settings_applications</mat-icon>
<div matListItemTitle>Dodatkowe ustawienia</div>
</button>
<button mat-list-item (click)="openAbout()"> <button mat-list-item (click)="openAbout()">
<mat-icon matListItemIcon>info</mat-icon> <mat-icon matListItemIcon>info</mat-icon>
<div matListItemTitle>O programie</div> <div matListItemTitle>O programie</div>

View File

@@ -3,23 +3,30 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PersonalComponent } from './personal.component'; import { PersonalComponent } from './personal.component';
import { AuthClient } from 'src/app/services/auth.client'; import { AuthClient } from 'src/app/services/auth.client';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { SwUpdate } from '@angular/service-worker';
import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatListModule } from '@angular/material/list'; import { MatListModule } from '@angular/material/list';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations';
import { AppUpdateService } from 'src/app/services/app-update.service';
import { of } from 'rxjs';
import { MatIconModule } from '@angular/material/icon';
describe('PersonalComponent', () => { describe('PersonalComponent', () => {
let component: PersonalComponent; let component: PersonalComponent;
let fixture: ComponentFixture<PersonalComponent>; let fixture: ComponentFixture<PersonalComponent>;
let auMock: jasmine.SpyObj<AppUpdateService>
beforeEach(() => { beforeEach(() => {
auMock = jasmine.createSpyObj("aumock", {
checkForUpdate: of()
})
const authMock = jasmine.createSpyObj('AuthClient', ['s']) const authMock = jasmine.createSpyObj('AuthClient', ['s'])
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [PersonalComponent], declarations: [PersonalComponent],
providers: [ providers: [
{provide: AuthClient, useValue: authMock}, {provide: AuthClient, useValue: authMock},
{provide: AppUpdateService, useValue: auMock}
], ],
imports: [MatDialogModule, MatSnackBarModule, MatListModule, BrowserAnimationsModule] imports: [MatDialogModule, MatSnackBarModule, MatListModule, NoopAnimationsModule, MatIconModule]
}); });
fixture = TestBed.createComponent(PersonalComponent); fixture = TestBed.createComponent(PersonalComponent);
component = fixture.componentInstance; component = fixture.componentInstance;

View File

@@ -10,6 +10,7 @@ import { LocalStorageService } from 'src/app/services/local-storage.service';
import { KeyComponent } from './key/key.component'; import { KeyComponent } from './key/key.component';
import { CleanComponent } from './clean/clean.component'; import { CleanComponent } from './clean/clean.component';
import { AboutComponent } from './about/about.component'; import { AboutComponent } from './about/about.component';
import { ExtraComponent } from './extra/extra.component';
@Component({ @Component({
selector: 'app-personal', selector: 'app-personal',
@@ -63,6 +64,10 @@ export class PersonalComponent {
this.ac.check() this.ac.check()
} }
protected openExtra() {
this.dialog.open(ExtraComponent)
}
protected openAbout() { protected openAbout() {
this.dialog.open(AboutComponent) this.dialog.open(AboutComponent)
} }

View File

@@ -1,6 +1,8 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { StartComponent } from './start.component'; import { StartComponent } from './start.component';
import { RouterModule } from '@angular/router';
import { MatListModule } from '@angular/material/list';
describe('StartComponent', () => { describe('StartComponent', () => {
let component: StartComponent; let component: StartComponent;
@@ -8,7 +10,11 @@ describe('StartComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [StartComponent] declarations: [StartComponent],
imports: [
RouterModule.forRoot([]),
MatListModule
]
}) })
.compileComponents(); .compileComponents();

View File

@@ -1,16 +1,22 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { AppUpdateService } from './services/app-update.service';
import { RouterModule } from '@angular/router';
describe('AppComponent', () => { describe('AppComponent', () => {
let auMock
beforeEach(async () => { beforeEach(async () => {
auMock = {}
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [ declarations: [
AppComponent AppComponent
], ],
providers: [
{provide: AppUpdateService, useValue: auMock}
],
imports: [
RouterModule
]
}).compileComponents(); }).compileComponents();
}); });
@@ -20,7 +26,7 @@ describe('AppComponent', () => {
expect(app).toBeTruthy(); expect(app).toBeTruthy();
}); });
it(`should have as title 'ipwa'`, () => { it(`should have as title 'Internat'`, () => {
const fixture = TestBed.createComponent(AppComponent); const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance; const app = fixture.componentInstance;
expect(app.title).toEqual('Internat'); expect(app.title).toEqual('Internat');

View File

@@ -78,6 +78,15 @@ import { AttendenceComponent } from './admin-view/grades/attendence/attendence.c
import { AttendenceSummaryComponent } from './admin-view/grades/attendence-summary/attendence-summary.component'; import { AttendenceSummaryComponent } from './admin-view/grades/attendence-summary/attendence-summary.component';
import { HourDisplayComponent } from './admin-view/grades/attendence-summary/hour-display/hour-display.component'; import { HourDisplayComponent } from './admin-view/grades/attendence-summary/hour-display/hour-display.component';
import { AboutComponent } from './app-view/personal/about/about.component'; import { AboutComponent } from './app-view/personal/about/about.component';
import { environment } from 'src/environments/environment';
import { ExtraComponent } from './app-view/personal/extra/extra.component';
import { RedirectComponent } from './app-view/personal/extra/redirect/redirect.component';
import { OutboxComponent } from './admin-view/notifications/outbox/outbox.component';
import { ToolbarComponent } from './admin-view/toolbar/toolbar.component';
import { MessageComponent } from './admin-view/notifications/outbox/message/message.component';
import { NotifDialogComponent } from './app-view/notif-dialog/notif-dialog.component';
import { UserSearchComponent } from './commonComponents/user-search/user-search.component';
import { StartAdminComponent } from './admin-view/start/start.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@@ -121,6 +130,14 @@ import { AboutComponent } from './app-view/personal/about/about.component';
AttendenceSummaryComponent, AttendenceSummaryComponent,
HourDisplayComponent, HourDisplayComponent,
AboutComponent, AboutComponent,
ExtraComponent,
RedirectComponent,
OutboxComponent,
ToolbarComponent,
MessageComponent,
NotifDialogComponent,
UserSearchComponent,
StartAdminComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@@ -160,7 +177,7 @@ import { AboutComponent } from './app-view/personal/about/about.component';
A11yModule, A11yModule,
MatAutocompleteModule, MatAutocompleteModule,
ServiceWorkerModule.register('ngsw-worker.js', { ServiceWorkerModule.register('ngsw-worker.js', {
enabled: !isDevMode(), enabled: environment.production,
// Register the ServiceWorker as soon as the application is stable // Register the ServiceWorker as soon as the application is stable
// or after 30 seconds (whichever comes first). // or after 30 seconds (whichever comes first).
registrationStrategy: 'registerWhenStable:30000' registrationStrategy: 'registerWhenStable:30000'

View File

@@ -13,7 +13,7 @@
<div cdkDropList class="vertical" (cdkDropListDropped)="drop($event)"> <div cdkDropList class="vertical" (cdkDropListDropped)="drop($event)">
@for (item of workList; track $index) { @for (item of workList; track $index) {
<span cdkDrag> <span cdkDrag>
<input type="text" [(ngModel)]="workList[$index]" [attr.list]="dataList"> <input type="text" [(ngModel)]="workList[$index]" [attr.list]="dataList" (keyup.enter)="addPos($index)" #input>
<select *ngIf="dropdown" [(ngModel)]="workList[$index]"> <select *ngIf="dropdown" [(ngModel)]="workList[$index]">
<option *ngFor="let option of options" [value]="option.id" [selected]="option.id == item">{{option.text}}</option> <option *ngFor="let option of options" [value]="option.id" [selected]="option.id == item">{{option.text}}</option>
</select> </select>

View File

@@ -1,5 +1,5 @@
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { Component, ElementRef, EventEmitter, HostBinding, HostListener, Input, OnChanges, Output, SimpleChanges, ViewChild } from '@angular/core'; import { ChangeDetectorRef, Component, ElementRef, EventEmitter, HostBinding, HostListener, Input, OnChanges, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core';
@Component({ @Component({
selector: 'app-list-editor[list], app-list-editor[converter]', selector: 'app-list-editor[list], app-list-editor[converter]',
@@ -15,10 +15,15 @@ export class ListEditorComponent implements OnChanges {
@Input() dropdown?: boolean; @Input() dropdown?: boolean;
@Input() dataList?: string; @Input() dataList?: string;
@Output() edit = new EventEmitter<string[]>(); @Output() edit = new EventEmitter<string[]>();
@ViewChildren('input') inputList!: QueryList<ElementRef>
protected _list: string[] = []; protected _list: string[] = [];
workList: string[] = []; workList: string[] = [];
focused = false; focused = false;
constructor (private cdRef: ChangeDetectorRef) {}
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (this.list) { if (this.list) {
this._list = [...this.list] this._list = [...this.list]
@@ -54,6 +59,8 @@ export class ListEditorComponent implements OnChanges {
addPos(index: number) { addPos(index: number) {
this.workList.splice(index+1, 0, '') this.workList.splice(index+1, 0, '')
this.cdRef.detectChanges()
this.inputList.get(index+1)?.nativeElement.focus()
} }
trackByIndex(index: number, _entry:any) { trackByIndex(index: number, _entry:any) {

View File

@@ -1,6 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RoomChooserComponent } from './room-chooser.component'; import { RoomChooserComponent } from './room-chooser.component';
import { MatIconModule } from '@angular/material/icon';
describe('RoomChooserComponent', () => { describe('RoomChooserComponent', () => {
let component: RoomChooserComponent; let component: RoomChooserComponent;
@@ -8,7 +9,8 @@ describe('RoomChooserComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [RoomChooserComponent] declarations: [RoomChooserComponent],
imports: [MatIconModule]
}) })
.compileComponents(); .compileComponents();

Some files were not shown because too many files have changed in this diff Show More