fix: Remade accmgmt comm

This commit is contained in:
2025-06-12 11:53:14 +02:00
parent 620717a534
commit 003ff86d18
12 changed files with 207 additions and 63 deletions

View File

@@ -1,37 +1,39 @@
<div id="upper-bar">
<mat-form-field>
<mat-form-field subscriptSizing="dynamic">
<mat-label>Wyszukaj</mat-label>
<input matInput (keyup)="filter($event)">
</mat-form-field>
<button mat-icon-button (click)="openUserCard()"><mat-icon>add</mat-icon></button>
</div>
@if (loading) {
<mat-spinner></mat-spinner>
}
<table mat-table [dataSource]="users">
<div matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Imię</th>
<td mat-cell *matCellDef="let element">{{element.fname}}</td>
</div>
<div matColumnDef="surname">
<th mat-header-cell *matHeaderCellDef>Nazwisko</th>
<td mat-cell *matCellDef="let element">{{element.surname}}</td>
</div>
<div matColumnDef="room">
<th mat-header-cell *matHeaderCellDef>Pokój</th>
<td mat-cell *matCellDef="let element">{{element.room}}</td>
</div>
<div matColumnDef="uname">
<th mat-header-cell *matHeaderCellDef>Nazwa użytkownika</th>
<td mat-cell *matCellDef="let element">{{element.uname}}</td>
</div>
<div matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Karta użytkownika</th>
<td mat-cell *matCellDef="let element">
<button mat-mini-fab (click)="openUserCard(element._id)"><mat-icon>manage_accounts</mat-icon></button>
</td>
</div>
<tr mat-header-row *matHeaderRowDef="collumns"></tr>
<tr mat-row *matRowDef="let row; columns: collumns"></tr>
</table>
<mat-paginator pageSize="9" [pageSizeOptions]="[9, 15, 20, 50, 160]"></mat-paginator>
<div class="mainc">
@if (ac.state() != STATE.LOADED) {
<app-load-shade [state]="ac.state()" [error]="ac.error()" (refresh)="ac.refresh()"/>
}
<table mat-table [dataSource]="users">
<div matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Imię</th>
<td mat-cell *matCellDef="let element">{{element.fname}}</td>
</div>
<div matColumnDef="surname">
<th mat-header-cell *matHeaderCellDef>Nazwisko</th>
<td mat-cell *matCellDef="let element">{{element.surname}}</td>
</div>
<div matColumnDef="room">
<th mat-header-cell *matHeaderCellDef>Pokój</th>
<td mat-cell *matCellDef="let element">{{element.room}}</td>
</div>
<div matColumnDef="uname">
<th mat-header-cell *matHeaderCellDef>Nazwa użytkownika</th>
<td mat-cell *matCellDef="let element">{{element.uname}}</td>
</div>
<div matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Karta użytkownika</th>
<td mat-cell *matCellDef="let element">
<button mat-mini-fab (click)="openUserCard(element._id)"><mat-icon>manage_accounts</mat-icon></button>
</td>
</div>
<tr mat-header-row *matHeaderRowDef="collumns"></tr>
<tr mat-row *matRowDef="let row; columns: collumns"></tr>
</table>
<mat-paginator pageSize="9" [pageSizeOptions]="[9, 15, 20, 50, 160]"></mat-paginator>
</div>

View File

@@ -4,6 +4,13 @@
height: 100%;
}
.mainc {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
}
mat-paginator {
margin-top: auto;
}

View File

@@ -6,26 +6,31 @@ import { MatSnackBarModule } from '@angular/material/snack-bar'
import { MatFormFieldModule } from '@angular/material/form-field'
import { MatIconModule } from '@angular/material/icon'
import { MatPaginatorModule } from '@angular/material/paginator'
import { of } from 'rxjs'
import { MatTableModule } from '@angular/material/table'
import { MatInputModule } from '@angular/material/input'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'
import { AccountMgmtService } from './account-mgmt.service'
import { LoadShadeComponent } from 'src/app/commonComponents/load-shade/load-shade.component'
import { of } from 'rxjs'
import { signal } from '@angular/core'
import { STATE } from 'src/app/types/state'
xdescribe('AccountMgmtComponent', () => {
describe('AccountMgmtComponent', () => {
let component: AccountMgmtComponent
let fixture: ComponentFixture<AccountMgmtComponent>
let acMock
beforeEach(async () => {
acMock = {
accs: {
getAccs: jasmine.createSpy('getAccs').and.returnValue(of()),
},
accs: of([]),
state: signal(STATE.NOT_LOADED),
refresh: jasmine.createSpy('getAccs'),
error: signal(undefined)
}
await TestBed.configureTestingModule({
declarations: [AccountMgmtComponent],
// providers: [{ provide: AdminCommService, useValue: acMock }],
declarations: [AccountMgmtComponent, LoadShadeComponent],
providers: [{ provide: AccountMgmtService, useValue: acMock }],
imports: [
MatDialogModule,
MatSnackBarModule,

View File

@@ -1,13 +1,13 @@
import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'
import { AfterViewInit, Component, ViewChild } from '@angular/core'
import { MatDialog } from '@angular/material/dialog'
import { MatTableDataSource } from '@angular/material/table'
import { MatPaginator } from '@angular/material/paginator'
import { MatSnackBar } from '@angular/material/snack-bar'
import { UserEditComponent } from './user-edit/user-edit.component'
import { LocalStorageService } from 'src/app/services/local-storage.service'
import { Group } from 'src/app/types/group'
import User from 'src/app/types/user'
import { AccountMgmtService } from './account-mgmt.service'
import { STATE } from 'src/app/types/state'
@Component({
selector: 'app-account-mgmt',
@@ -15,16 +15,14 @@ import { AccountMgmtService } from './account-mgmt.service'
styleUrls: ['./account-mgmt.component.scss'],
standalone: false,
})
export class AccountMgmtComponent implements OnInit, AfterViewInit {
export class AccountMgmtComponent implements AfterViewInit {
protected groups: Group[] = []
users: MatTableDataSource<Omit<User, 'pass'>>
loading = false
@ViewChild(MatPaginator) paginator!: MatPaginator
constructor(
readonly ac: AccountMgmtService,
protected ac: AccountMgmtService,
private dialog: MatDialog,
private sb: MatSnackBar,
protected readonly ls: LocalStorageService
) {
this.users = new MatTableDataSource<Omit<User, 'pass'>>()
@@ -43,21 +41,20 @@ export class AccountMgmtComponent implements OnInit, AfterViewInit {
const filternew = filter.trim().toLowerCase()
return dataStr.indexOf(filternew) != -1
}
this.ac.refresh()
this.ac.accs.subscribe(d => {
this.users.data = d ?? []
})
}
protected get STATE(): typeof STATE {
return STATE
}
ngAfterViewInit() {
this.users.paginator = this.paginator
}
ngOnInit() {
this.loading = true
this.ac.getAccs().subscribe(data => {
this.loading = false
this.users.data = data.users
this.groups = data.groups
})
}
filter(event: Event) {
const value = (event.target as HTMLInputElement).value
this.users.filter = value.toLowerCase().trim()
@@ -74,7 +71,7 @@ export class AccountMgmtComponent implements OnInit, AfterViewInit {
})
.afterClosed()
.subscribe(r => {
if (r) this.ngOnInit()
if (r) this.ac.refresh()
})
}

View File

@@ -1,16 +1,43 @@
import { TestBed } from '@angular/core/testing';
import { AccountMgmtService } from './account-mgmt.service';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { environment } from 'src/environments/environment';
xdescribe('AccountMgmtService', () => {
describe('AccountMgmtService', () => {
let service: AccountMgmtService;
let httpTesting: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({});
TestBed.configureTestingModule({
providers: [
provideHttpClient(),
provideHttpClientTesting()
]
});
httpTesting = TestBed.inject(HttpTestingController);
service = TestBed.inject(AccountMgmtService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should get user accounts', () => {
service.refresh()
const req = httpTesting.expectOne(environment.apiEndpoint+"/admin/accs", "Request to load all users")
expect(req.request.method).toBe("GET")
req.flush([])
httpTesting.verify()
})
it('should create a user account and refresh list', () => {
service.postAcc({
uname: "test",
groups: []
})
})
});

View File

@@ -1,6 +1,8 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Injectable, signal } from '@angular/core';
import { BehaviorSubject, catchError, of } from 'rxjs';
import { Group } from 'src/app/types/group';
import { STATE } from 'src/app/types/state';
import { Status } from 'src/app/types/status';
import User from 'src/app/types/user';
import { environment } from 'src/environments/environment';
@@ -12,15 +14,37 @@ export class AccountMgmtService {
constructor(private http: HttpClient) { }
getAccs() {
return this.http.get<{
private _accs = new BehaviorSubject<Omit<User, 'pass'>[]>([])
public readonly accs = this._accs.asObservable()
private _state = signal(STATE.NOT_LOADED);
public readonly state = this._state.asReadonly();
private _error = signal<string | undefined>(undefined);
public readonly error = this._error.asReadonly();
public refresh() {
this.getAccs()
}
private getAccs() {
this._state.set(STATE.PENDING)
this.http.get<{
users: Omit<User, 'pass'>[]
groups: Group[]
}>(environment.apiEndpoint + `/admin/accs`, { withCredentials: true })
.pipe(catchError((err: Error) => {
this._state.set(STATE.ERROR)
this._error.set(err.message)
return of()
}))
.subscribe(v => {
this._error.set(undefined)
this._accs.next(v.users ?? [])
this._state.set(STATE.LOADED)
})
}
postAcc(item: any) {
return this.http.post<Status>(
postAcc(item: Omit<User, "pass" | "_id" | "regDate">) {
return this.http.post<Omit<User, "pass">>(
environment.apiEndpoint + `/admin/accs`,
item,
{ withCredentials: true }
@@ -38,7 +62,7 @@ export class AccountMgmtService {
resetPass(id: string) {
return this.http.patch<Status>(
environment.apiEndpoint + `/admin/accs/${id}/reset`,
{},
undefined,
{ withCredentials: true }
)
}

View File

@@ -85,7 +85,8 @@ import { MessageComponent } from './admin-view/notifications/outbox/message/mess
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'
import { provideLuxonDateAdapter } from '@angular/material-luxon-adapter'
import { provideLuxonDateAdapter } from '@angular/material-luxon-adapter';
import { LoadShadeComponent } from './commonComponents/load-shade/load-shade.component'
@NgModule({
declarations: [
@@ -137,6 +138,7 @@ import { provideLuxonDateAdapter } from '@angular/material-luxon-adapter'
NotifDialogComponent,
UserSearchComponent,
StartAdminComponent,
LoadShadeComponent,
],
bootstrap: [AppComponent],
imports: [

View File

@@ -0,0 +1,22 @@
@switch (state()) {
@case (STATE.PENDING) {
<mat-spinner color="accent"></mat-spinner>
}
@case (STATE.ERROR) {
<mat-card>
<mat-card-header>
<mat-card-title>Błąd wczytywania</mat-card-title>
<mat-card-subtitle>Podczas wczytywania treści wystąpił błąd.</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<p>{{ error() }}</p>
</mat-card-content>
<mat-card-actions align="end">
<button (click)="refresh.emit()" mat-flat-button color="accent">Odśwież</button>
</mat-card-actions>
</mat-card>
}
@default {
<h1>{{state()}}</h1>
}
}

View File

@@ -0,0 +1,11 @@
:host {
position: absolute;
background-color: hsla(0, 0%, 0%, 0.5);
top: 0;
bottom: 0;
right: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
}

View File

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

View File

@@ -0,0 +1,18 @@
import { Component, input, output } from '@angular/core';
import { STATE } from 'src/app/types/state';
@Component({
selector: 'app-load-shade',
standalone: false,
templateUrl: './load-shade.component.html',
styleUrl: './load-shade.component.scss',
})
export class LoadShadeComponent {
state = input(STATE.NOT_LOADED)
error = input<string | undefined>(undefined)
refresh = output<void>()
public get STATE(): typeof STATE {
return STATE
}
}

6
src/app/types/state.ts Normal file
View File

@@ -0,0 +1,6 @@
export enum STATE {
NOT_LOADED,
PENDING,
LOADED,
ERROR
}