feat: add admin user frontend contract
This commit is contained in:
@@ -28,6 +28,17 @@ describe('app routing', () => {
|
|||||||
expect(router.url).toBe('/auth/login');
|
expect(router.url).toBe('/auth/login');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('redirects the protected users route to login when logged out', async () => {
|
||||||
|
await router.navigateByUrl('/users');
|
||||||
|
expect(router.url).toBe('/auth/login');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows an authenticated user to open the users route', async () => {
|
||||||
|
authStore.setSession('token', { id: 1, email: 'a@b.de', firstName: 'A', lastName: 'B' });
|
||||||
|
await router.navigateByUrl('/users');
|
||||||
|
expect(router.url).toBe('/users');
|
||||||
|
});
|
||||||
|
|
||||||
it('redirects the root path to team-select when logged in', async () => {
|
it('redirects the root path to team-select when logged in', async () => {
|
||||||
authStore.setSession('token', { id: 1, email: 'a@b.de', firstName: 'A', lastName: 'B' });
|
authStore.setSession('token', { id: 1, email: 'a@b.de', firstName: 'A', lastName: 'B' });
|
||||||
await router.navigateByUrl('/');
|
await router.navigateByUrl('/');
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ export const routes: Routes = [
|
|||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
loadComponent: () => import('./features/team-select/team-select').then((m) => m.TeamSelect),
|
loadComponent: () => import('./features/team-select/team-select').then((m) => m.TeamSelect),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'users',
|
||||||
|
canActivate: [authGuard],
|
||||||
|
loadComponent: () => import('./features/users/users').then((m) => m.Users),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 't/:token/:playerId',
|
path: 't/:token/:playerId',
|
||||||
loadComponent: () => import('./features/public-team/public-player').then((m) => m.PublicPlayer),
|
loadComponent: () => import('./features/public-team/public-player').then((m) => m.PublicPlayer),
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export interface RegistrationRequest {
|
|||||||
password: string;
|
password: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
linkPlayerId: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateInviteRequest extends InviteDetails {}
|
export interface CreateInviteRequest extends InviteDetails {}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { AuthStore } from './auth-store';
|
import { AuthStore } from './auth-store';
|
||||||
|
import { RoleId } from '../../models/role.model';
|
||||||
|
|
||||||
describe('AuthStore', () => {
|
describe('AuthStore', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -76,4 +77,34 @@ describe('AuthStore', () => {
|
|||||||
expect(localStorage.getItem('tw_token')).toBe('jwt-token');
|
expect(localStorage.getItem('tw_token')).toBe('jwt-token');
|
||||||
expect(JSON.parse(localStorage.getItem('tw_user')!)).toEqual(updated);
|
expect(JSON.parse(localStorage.getItem('tw_user')!)).toEqual(updated);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('derives the global admin presentation hint from the current user role', () => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
const store = TestBed.inject(AuthStore);
|
||||||
|
store.setSession('jwt-token', {
|
||||||
|
id: 1,
|
||||||
|
email: 'admin@example.de',
|
||||||
|
firstName: 'Ada',
|
||||||
|
lastName: 'Admin',
|
||||||
|
role: { id: RoleId.Admin, name: 'admin' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(store.currentGlobalRole()).toBe(RoleId.Admin);
|
||||||
|
expect(store.isGlobalAdmin()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not treat a signed-in standard user as a global admin', () => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
const store = TestBed.inject(AuthStore);
|
||||||
|
store.setSession('jwt-token', {
|
||||||
|
id: 2,
|
||||||
|
email: 'user@example.de',
|
||||||
|
firstName: 'Ute',
|
||||||
|
lastName: 'User',
|
||||||
|
role: { id: RoleId.User, name: 'user' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(store.currentGlobalRole()).toBe(RoleId.User);
|
||||||
|
expect(store.isGlobalAdmin()).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Injectable, computed, signal } from '@angular/core';
|
import { Injectable, computed, signal } from '@angular/core';
|
||||||
|
import { RoleId } from '../../models/role.model';
|
||||||
import { User } from '../../models/user.model';
|
import { User } from '../../models/user.model';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
@@ -12,6 +13,11 @@ export class AuthStore {
|
|||||||
readonly token = this.tokenSignal.asReadonly();
|
readonly token = this.tokenSignal.asReadonly();
|
||||||
readonly currentUser = this.userSignal.asReadonly();
|
readonly currentUser = this.userSignal.asReadonly();
|
||||||
readonly isLoggedIn = computed(() => this.tokenSignal() !== null);
|
readonly isLoggedIn = computed(() => this.tokenSignal() !== null);
|
||||||
|
readonly currentGlobalRole = computed<RoleId | null>(() => {
|
||||||
|
const roleId = this.userSignal()?.role?.id;
|
||||||
|
return roleId === RoleId.Admin || roleId === RoleId.User ? roleId : null;
|
||||||
|
});
|
||||||
|
readonly isGlobalAdmin = computed(() => this.currentGlobalRole() === RoleId.Admin);
|
||||||
|
|
||||||
setSession(token: string, user: User): void {
|
setSession(token: string, user: User): void {
|
||||||
localStorage.setItem(AuthStore.TOKEN_KEY, token);
|
localStorage.setItem(AuthStore.TOKEN_KEY, token);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { MatToolbarModule } from '@angular/material/toolbar';
|
|||||||
import { AuthStore } from '../../auth/auth-store';
|
import { AuthStore } from '../../auth/auth-store';
|
||||||
import { MyTeamsStore } from '../../team/my-teams-store';
|
import { MyTeamsStore } from '../../team/my-teams-store';
|
||||||
import { TeamStore } from '../../team/team-store';
|
import { TeamStore } from '../../team/team-store';
|
||||||
import { Team } from '../../../models/team.model';
|
import { UserTeamReference } from '../../../models/user-directory.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-shell',
|
selector: 'app-shell',
|
||||||
@@ -41,9 +41,9 @@ export class Shell {
|
|||||||
|
|
||||||
protected readonly myTeams = computed(() => {
|
protected readonly myTeams = computed(() => {
|
||||||
const seen = new Set<number>();
|
const seen = new Set<number>();
|
||||||
const teams: Team[] = [];
|
const teams: UserTeamReference[] = [];
|
||||||
for (const player of this.myTeamsStore.players()) {
|
for (const player of this.myTeamsStore.players()) {
|
||||||
if (player.team && !seen.has(player.team.id)) {
|
if (!seen.has(player.team.id)) {
|
||||||
seen.add(player.team.id);
|
seen.add(player.team.id);
|
||||||
teams.push(player.team);
|
teams.push(player.team);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,19 +3,16 @@ import { provideHttpClient } from '@angular/common/http';
|
|||||||
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||||
import { MyTeamsStore } from './my-teams-store';
|
import { MyTeamsStore } from './my-teams-store';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { Player } from '../../models/player.model';
|
|
||||||
|
|
||||||
describe('MyTeamsStore', () => {
|
describe('MyTeamsStore', () => {
|
||||||
let store: MyTeamsStore;
|
let store: MyTeamsStore;
|
||||||
let httpMock: HttpTestingController;
|
let httpMock: HttpTestingController;
|
||||||
|
|
||||||
const player: Player = {
|
const player = {
|
||||||
id: 1,
|
id: 1,
|
||||||
firstName: 'A',
|
firstName: 'A',
|
||||||
lastName: 'B',
|
lastName: 'B',
|
||||||
balance: 0,
|
team: { id: 5, name: 'Team A' },
|
||||||
active: true,
|
|
||||||
team: { id: 5, name: 'Team A', alias: 'team-a', balance: 0 },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Injectable, inject, signal } from '@angular/core';
|
import { Injectable, inject, signal } from '@angular/core';
|
||||||
import { Player } from '../../models/player.model';
|
import { UserTeamMembership } from '../../models/user-directory.model';
|
||||||
import { TeamsApi } from './teams-api';
|
import { TeamsApi } from './teams-api';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class MyTeamsStore {
|
export class MyTeamsStore {
|
||||||
private readonly teamsApi = inject(TeamsApi);
|
private readonly teamsApi = inject(TeamsApi);
|
||||||
|
|
||||||
private readonly playersSignal = signal<Player[]>([]);
|
private readonly playersSignal = signal<UserTeamMembership[]>([]);
|
||||||
private readonly loadingSignal = signal(false);
|
private readonly loadingSignal = signal(false);
|
||||||
private readonly loadedForUserId = signal<number | null>(null);
|
private readonly loadedForUserId = signal<number | null>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ describe('TeamsApi', () => {
|
|||||||
httpMock.verify();
|
httpMock.verify();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fetches the players/teams belonging to a user', () => {
|
it('fetches the reduced bootstrap memberships belonging to a user', () => {
|
||||||
const players: Player[] = [{ id: 1, firstName: 'A', lastName: 'B', balance: 0, active: true }];
|
const players = [{ id: 1, firstName: 'A', lastName: 'B', team: { id: 5, name: 'Team A' } }];
|
||||||
|
|
||||||
service.loadMyTeams(42).subscribe((response) => {
|
service.loadMyTeams(42).subscribe((response) => {
|
||||||
expect(response).toEqual(players);
|
expect(response).toEqual(players);
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { HttpClient } from '@angular/common/http';
|
|||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { Player } from '../../models/player.model';
|
|
||||||
import { Team } from '../../models/team.model';
|
import { Team } from '../../models/team.model';
|
||||||
import { PlayerTransaction } from '../../models/transaction.model';
|
import { PlayerTransaction } from '../../models/transaction.model';
|
||||||
|
import { UserTeamMembership } from '../../models/user-directory.model';
|
||||||
|
import { Player } from '../../models/player.model';
|
||||||
|
|
||||||
export interface CreatePlayerRequest {
|
export interface CreatePlayerRequest {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
@@ -16,8 +17,8 @@ export interface CreatePlayerRequest {
|
|||||||
export class TeamsApi {
|
export class TeamsApi {
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
loadMyTeams(userId: number): Observable<Player[]> {
|
loadMyTeams(userId: number): Observable<UserTeamMembership[]> {
|
||||||
return this.http.get<Player[]>(`${environment.apiUrl}users/${userId}/teams`);
|
return this.http.get<UserTeamMembership[]>(`${environment.apiUrl}users/${userId}/teams`);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadTeamOverview(teamId: number): Observable<Team> {
|
loadTeamOverview(teamId: number): Observable<Team> {
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import {
|
||||||
|
AdminPlayerFilters,
|
||||||
|
AdminPlayerPage,
|
||||||
|
AdminUserDirectorySummary,
|
||||||
|
AdminUserProfileRequest,
|
||||||
|
AdminUserRoleRequest,
|
||||||
|
AdminUserStatusRequest,
|
||||||
|
} from '../../models/user-directory.model';
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AdminUsersApi {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
private readonly baseUrl = `${environment.apiUrl}admin/users`;
|
||||||
|
|
||||||
|
updateProfile(userId: number, request: AdminUserProfileRequest): Observable<AdminUserDirectorySummary> {
|
||||||
|
return this.http.patch<AdminUserDirectorySummary>(`${this.baseUrl}/${userId}/profile`, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRole(userId: number, request: AdminUserRoleRequest): Observable<AdminUserDirectorySummary> {
|
||||||
|
return this.http.patch<AdminUserDirectorySummary>(`${this.baseUrl}/${userId}/role`, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus(userId: number, request: AdminUserStatusRequest): Observable<AdminUserDirectorySummary> {
|
||||||
|
return this.http.patch<AdminUserDirectorySummary>(`${this.baseUrl}/${userId}/status`, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPlayers(filters: AdminPlayerFilters = {}): Observable<AdminPlayerPage> {
|
||||||
|
return this.http.get<AdminPlayerPage>(`${this.baseUrl}/players`, { params: this.toParams(filters) });
|
||||||
|
}
|
||||||
|
|
||||||
|
assignPlayer(userId: number, playerId: number): Observable<AdminUserDirectorySummary> {
|
||||||
|
return this.http.put<AdminUserDirectorySummary>(`${this.baseUrl}/${userId}/players/${playerId}`, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
unlinkPlayer(userId: number, playerId: number): Observable<AdminUserDirectorySummary> {
|
||||||
|
return this.http.delete<AdminUserDirectorySummary>(`${this.baseUrl}/${userId}/players/${playerId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private toParams(filters: AdminPlayerFilters): HttpParams {
|
||||||
|
let params = new HttpParams();
|
||||||
|
if (filters.search !== undefined) params = params.set('search', filters.search);
|
||||||
|
if (filters.teamId !== undefined) params = params.set('teamId', filters.teamId);
|
||||||
|
if (filters.assignment !== undefined) params = params.set('assignment', filters.assignment);
|
||||||
|
if (filters.page !== undefined) params = params.set('page', filters.page);
|
||||||
|
if (filters.limit !== undefined) params = params.set('limit', filters.limit);
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { provideHttpClient } from '@angular/common/http';
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { AdminUsersApi } from './admin-users-api';
|
||||||
|
import { UsersApi } from './users-api';
|
||||||
|
|
||||||
|
describe('UsersApi', () => {
|
||||||
|
let service: UsersApi;
|
||||||
|
let httpMock: HttpTestingController;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting()] });
|
||||||
|
service = TestBed.inject(UsersApi);
|
||||||
|
httpMock = TestBed.inject(HttpTestingController);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => httpMock.verify());
|
||||||
|
|
||||||
|
it('loads the safe user directory with supplied pagination and search filters', () => {
|
||||||
|
service.loadDirectory({ page: 2, limit: 10, search: 'Alex Muster' }).subscribe();
|
||||||
|
|
||||||
|
const request = httpMock.expectOne(
|
||||||
|
`${environment.apiUrl}users/directory?page=2&limit=10&search=Alex%20Muster`,
|
||||||
|
);
|
||||||
|
expect(request.request.method).toBe('GET');
|
||||||
|
request.flush({ data: [], page: 2, limit: 10, total: 0, hasNextPage: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('AdminUsersApi', () => {
|
||||||
|
let service: AdminUsersApi;
|
||||||
|
let httpMock: HttpTestingController;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting()] });
|
||||||
|
service = TestBed.inject(AdminUsersApi);
|
||||||
|
httpMock = TestBed.inject(HttpTestingController);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => httpMock.verify());
|
||||||
|
|
||||||
|
it('uses the dedicated admin profile endpoint and payload', () => {
|
||||||
|
service.updateProfile(7, { firstName: 'Alex', lastName: null }).subscribe();
|
||||||
|
const request = httpMock.expectOne(`${environment.apiUrl}admin/users/7/profile`);
|
||||||
|
expect(request.request.method).toBe('PATCH');
|
||||||
|
expect(request.request.body).toEqual({ firstName: 'Alex', lastName: null });
|
||||||
|
request.flush({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the dedicated admin role endpoint and payload', () => {
|
||||||
|
service.updateRole(7, { role: 1 }).subscribe();
|
||||||
|
const request = httpMock.expectOne(`${environment.apiUrl}admin/users/7/role`);
|
||||||
|
expect(request.request.method).toBe('PATCH');
|
||||||
|
expect(request.request.body).toEqual({ role: 1 });
|
||||||
|
request.flush({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the dedicated admin status endpoint and payload', () => {
|
||||||
|
service.updateStatus(7, { status: 2 }).subscribe();
|
||||||
|
const request = httpMock.expectOne(`${environment.apiUrl}admin/users/7/status`);
|
||||||
|
expect(request.request.method).toBe('PATCH');
|
||||||
|
expect(request.request.body).toEqual({ status: 2 });
|
||||||
|
request.flush({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads players with the admin filters', () => {
|
||||||
|
service.loadPlayers({ search: 'Alex', teamId: 5, assignment: 'unassigned', page: 2, limit: 10 }).subscribe();
|
||||||
|
const request = httpMock.expectOne(
|
||||||
|
`${environment.apiUrl}admin/users/players?search=Alex&teamId=5&assignment=unassigned&page=2&limit=10`,
|
||||||
|
);
|
||||||
|
expect(request.request.method).toBe('GET');
|
||||||
|
request.flush({ data: [], page: 2, limit: 10, total: 0, hasNextPage: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('assigns a player through the dedicated admin endpoint', () => {
|
||||||
|
service.assignPlayer(7, 9).subscribe();
|
||||||
|
const request = httpMock.expectOne(`${environment.apiUrl}admin/users/7/players/9`);
|
||||||
|
expect(request.request.method).toBe('PUT');
|
||||||
|
expect(request.request.body).toBeNull();
|
||||||
|
request.flush({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unlinks a player through the dedicated admin endpoint', () => {
|
||||||
|
service.unlinkPlayer(7, 9).subscribe();
|
||||||
|
const request = httpMock.expectOne(`${environment.apiUrl}admin/users/7/players/9`);
|
||||||
|
expect(request.request.method).toBe('DELETE');
|
||||||
|
request.flush({});
|
||||||
|
});
|
||||||
|
});
|
||||||
25
myteamwallet_frontend_modern/src/app/core/users/users-api.ts
Normal file
25
myteamwallet_frontend_modern/src/app/core/users/users-api.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { UserDirectoryFilters, UserDirectoryPage } from '../../models/user-directory.model';
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class UsersApi {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
private readonly baseUrl = `${environment.apiUrl}users`;
|
||||||
|
|
||||||
|
loadDirectory(filters: UserDirectoryFilters = {}): Observable<UserDirectoryPage> {
|
||||||
|
return this.http.get<UserDirectoryPage>(`${this.baseUrl}/directory`, {
|
||||||
|
params: this.toParams(filters),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private toParams(filters: UserDirectoryFilters): HttpParams {
|
||||||
|
let params = new HttpParams();
|
||||||
|
if (filters.page !== undefined) params = params.set('page', filters.page);
|
||||||
|
if (filters.limit !== undefined) params = params.set('limit', filters.limit);
|
||||||
|
if (filters.search !== undefined) params = params.set('search', filters.search);
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ describe('Register', () => {
|
|||||||
|
|
||||||
afterEach(() => httpMock.verify());
|
afterEach(() => httpMock.verify());
|
||||||
|
|
||||||
it('verifies the invitation and links its player during registration', () => {
|
it('verifies the invitation and registers without linking its player', () => {
|
||||||
const fixture = TestBed.createComponent(Register);
|
const fixture = TestBed.createComponent(Register);
|
||||||
const navigateSpy = vi.spyOn(router, 'navigate');
|
const navigateSpy = vi.spyOn(router, 'navigate');
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -50,7 +50,12 @@ describe('Register', () => {
|
|||||||
fixture.componentInstance['onSubmit']();
|
fixture.componentInstance['onSubmit']();
|
||||||
|
|
||||||
const request = httpMock.expectOne(`${environment.apiUrl}auth/email/register`);
|
const request = httpMock.expectOne(`${environment.apiUrl}auth/email/register`);
|
||||||
expect(request.request.body.linkPlayerId).toBe(7);
|
expect(request.request.body).toEqual({
|
||||||
|
email: 'alex@example.de',
|
||||||
|
password: 'secret1',
|
||||||
|
firstName: 'Alex',
|
||||||
|
lastName: 'Muster',
|
||||||
|
});
|
||||||
request.flush(null);
|
request.flush(null);
|
||||||
|
|
||||||
expect(navigateSpy).toHaveBeenCalledWith(['/auth/login'], { replaceUrl: true });
|
expect(navigateSpy).toHaveBeenCalledWith(['/auth/login'], { replaceUrl: true });
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ export class Register {
|
|||||||
password: value.password,
|
password: value.password,
|
||||||
firstName: value.firstName,
|
firstName: value.firstName,
|
||||||
lastName: value.lastName,
|
lastName: value.lastName,
|
||||||
linkPlayerId: invitation.playerId,
|
|
||||||
})
|
})
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
<h1>Team auswählen</h1>
|
<h1>Team auswählen</h1>
|
||||||
<mat-nav-list>
|
<mat-nav-list>
|
||||||
@for (player of players(); track player.id) {
|
@for (player of players(); track player.id) {
|
||||||
<a mat-list-item [routerLink]="['/team', player.team?.id, 'overview']">
|
<a mat-list-item [routerLink]="['/team', player.team.id, 'overview']">
|
||||||
<span matListItemTitle>{{ player.team?.name }}</span>
|
<span matListItemTitle>{{ player.team.name }}</span>
|
||||||
<span matListItemLine>{{ player.firstName }} {{ player.lastName }}</span>
|
<span matListItemLine>{{ player.firstName }} {{ player.lastName }}</span>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,13 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="link-grid">
|
<section class="link-grid">
|
||||||
|
<a routerLink="/users"
|
||||||
|
><mat-card
|
||||||
|
><mat-icon>group</mat-icon>
|
||||||
|
<div><strong>Benutzer</strong><span>Benutzerverzeichnis öffnen</span></div>
|
||||||
|
<mat-icon>chevron_right</mat-icon></mat-card
|
||||||
|
></a
|
||||||
|
>
|
||||||
<a routerLink="penalties"
|
<a routerLink="penalties"
|
||||||
><mat-card
|
><mat-card
|
||||||
><mat-icon>gavel</mat-icon>
|
><mat-icon>gavel</mat-icon>
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ describe('More', () => {
|
|||||||
expect(fixture.nativeElement.textContent).toContain('Profil');
|
expect(fixture.nativeElement.textContent).toContain('Profil');
|
||||||
expect(fixture.nativeElement.textContent).toContain('Öffentliche Freigabe');
|
expect(fixture.nativeElement.textContent).toContain('Öffentliche Freigabe');
|
||||||
|
|
||||||
|
expect(fixture.nativeElement.textContent).toContain('Benutzer');
|
||||||
|
expect(fixture.nativeElement.querySelector('a[href="/users"]')).not.toBeNull();
|
||||||
|
|
||||||
fixture.componentInstance['logout']();
|
fixture.componentInstance['logout']();
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
expect(clearSession).toHaveBeenCalled();
|
expect(clearSession).toHaveBeenCalled();
|
||||||
|
|||||||
13
myteamwallet_frontend_modern/src/app/features/users/users.ts
Normal file
13
myteamwallet_frontend_modern/src/app/features/users/users.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-users',
|
||||||
|
template: `
|
||||||
|
<header>
|
||||||
|
<p class="eyebrow">Organisation</p>
|
||||||
|
<h1>Benutzer</h1>
|
||||||
|
<p>Benutzerverzeichnis wird vorbereitet.</p>
|
||||||
|
</header>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class Users {}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
export interface UserDirectoryReference {
|
||||||
|
id: number;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserDirectoryTeam {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
alias: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserDirectoryAssignment {
|
||||||
|
id: number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
active: boolean;
|
||||||
|
team: UserDirectoryTeam;
|
||||||
|
teamRole: UserDirectoryReference | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserDirectorySummary {
|
||||||
|
id: number;
|
||||||
|
firstName: string | null;
|
||||||
|
lastName: string | null;
|
||||||
|
status: UserDirectoryReference | null;
|
||||||
|
assignments: UserDirectoryAssignment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminUserDirectorySummary extends UserDirectorySummary {
|
||||||
|
email: string | null;
|
||||||
|
role: UserDirectoryReference | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserDirectoryRecord = UserDirectorySummary | AdminUserDirectorySummary;
|
||||||
|
|
||||||
|
export interface UserDirectoryPage {
|
||||||
|
data: UserDirectoryRecord[];
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
total: number;
|
||||||
|
hasNextPage: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserDirectoryFilters {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
search?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminUserProfileRequest {
|
||||||
|
firstName?: string | null;
|
||||||
|
lastName?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminUserRoleRequest {
|
||||||
|
role: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminUserStatusRequest {
|
||||||
|
status: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AdminPlayerAssignmentFilter = 'all' | 'assigned' | 'unassigned';
|
||||||
|
|
||||||
|
export interface AdminPlayerFilters {
|
||||||
|
search?: string;
|
||||||
|
teamId?: number;
|
||||||
|
assignment?: AdminPlayerAssignmentFilter;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminPlayerTeam {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
alias: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminPlayerCurrentUser {
|
||||||
|
id: number;
|
||||||
|
firstName: string | null;
|
||||||
|
lastName: string | null;
|
||||||
|
status: UserDirectoryReference | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminPlayerSummary {
|
||||||
|
id: number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
active: boolean;
|
||||||
|
team: AdminPlayerTeam;
|
||||||
|
currentUser: AdminPlayerCurrentUser | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminPlayerPage {
|
||||||
|
data: AdminPlayerSummary[];
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
total: number;
|
||||||
|
hasNextPage: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserTeamReference {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserTeamMembership {
|
||||||
|
id: number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
team: UserTeamReference;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user