# Team erstellen über die UI Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Any logged-in user can create a new team via the Angular UI and automatically becomes its captain. **Architecture:** Loosen the existing `POST /teams` backend endpoint from admin-only to any authenticated user, and extend `TeamsService.createNewTeam` to also create a `Player` membership row (role = captain) for the calling user. On the frontend, add a `createTeam` API method, a small `MatDialog` form component, and wire a persistent "Team erstellen" button into `team-select` that opens the dialog, refreshes the user's team list, and navigates into the new team. **Tech Stack:** NestJS + TypeORM + Jest (backend, `myteamwallet_backend`); Angular 21 standalone components + Angular Material + signals + Vitest (frontend, `myteamwallet_frontend_modern`). ## Global Constraints - Spec: `docs/superpowers/specs/2026-08-03-team-erstellen-design.md`. - Creator's team role is always `captain` (`TeamRolesEnum.captain` = `3`, `myteamwallet_backend/src/team-roles/team-roles.enum.ts`) — no role choice in the UI. - `CreateTeamDTO` stays `{ name: string }` — no other fields are added to the create-team form. - Backend: any new constructor dependency on `TeamsService` must be appended as the **last** parameter — `teams.service.spec.ts` constructs `TeamsService` positionally in two places, and reordering breaks those mocks. - Backend tests use Jest (`npm run test` from `myteamwallet_backend/`); frontend tests use Vitest via the Angular CLI (`npm test` from `myteamwallet_frontend_modern/`). - All new user-facing copy is German, matching existing screens (e.g. "Team erstellen", "Teamname", "Erstellen", "Abbrechen"). - Out of scope (per spec): team deletion/archiving, custom alias entry, choosing the creator's role. --- ### Task 1: Loosen the `POST /teams` role guard to any logged-in user **Files:** - Modify: `myteamwallet_backend/src/teams/teams.controller.ts:247` - Test: `myteamwallet_backend/src/teams/teams.controller.spec.ts` (new file) **Interfaces:** - Consumes: nothing new. - Produces: nothing new — this task only changes an authorization decorator and adds a regression test for it. `TeamsController.create(req, teamDto)` keeps calling `this.service.createNewTeam(teamDto, userId)` unchanged. - [ ] **Step 1: Write the failing test** Create `myteamwallet_backend/src/teams/teams.controller.spec.ts`: ```ts import { GUARDS_METADATA } from '@nestjs/common/constants'; import { RoleEnum } from '../roles/roles.enum'; import { RolesGuard } from '../roles/roles.guard'; import { TeamsController } from './teams.controller'; describe('TeamsController', () => { const service = { createNewTeam: jest.fn() }; const publicAccess = {}; const teamMembers = {}; const teamPermissions = {}; const controller = new TeamsController( service as any, publicAccess as any, teamMembers as any, teamPermissions as any, ); beforeEach(() => jest.clearAllMocks()); it('allows any logged-in user (not just admins) to create a team', () => { expect(Reflect.getMetadata('roles', TeamsController.prototype.create)).toEqual([ RoleEnum.user, RoleEnum.admin, ]); expect( Reflect.getMetadata(GUARDS_METADATA, TeamsController.prototype.create), ).toContain(RolesGuard); }); it('passes the authenticated user and the DTO to the service', () => { const req = { user: { id: 42 } }; const dto = { name: '1. Herren' }; controller.create(req as any, dto as any); expect(service.createNewTeam).toHaveBeenCalledWith(dto, 42); }); }); ``` - [ ] **Step 2: Run test to verify it fails** Run (from `myteamwallet_backend/`): `npx jest teams.controller.spec.ts` Expected: FAIL on the first test — `Reflect.getMetadata('roles', ...)` currently equals `[RoleEnum.admin]`, not `[RoleEnum.user, RoleEnum.admin]`. - [ ] **Step 3: Loosen the guard** In `myteamwallet_backend/src/teams/teams.controller.ts`, change line 247 from: ```ts @Roles([RoleEnum.admin]) ``` to: ```ts @Roles([RoleEnum.user, RoleEnum.admin]) ``` (This is the only `@Roles([RoleEnum.admin])` occurrence in the file — it decorates the `create()` handler at lines 241-254.) - [ ] **Step 4: Run test to verify it passes** Run: `npx jest teams.controller.spec.ts` Expected: PASS (2 tests). - [ ] **Step 5: Commit** ```bash git add myteamwallet_backend/src/teams/teams.controller.ts myteamwallet_backend/src/teams/teams.controller.spec.ts git commit -m "feat: allow any logged-in user to create a team" ``` --- ### Task 2: Make the creator a captain of the new team **Files:** - Modify: `myteamwallet_backend/src/teams/teams.service.ts` - Modify: `myteamwallet_backend/src/teams/teams.service.spec.ts` **Interfaces:** - Consumes: `TeamRolesEnum.captain` (`myteamwallet_backend/src/team-roles/team-roles.enum.ts`, value `3`); `User` entity (`myteamwallet_backend/src/users/entities/user.entity.ts`, has `firstName: string | null`, `lastName: string | null`); `Player` entity (`myteamwallet_backend/src/players/entities/player.entity.ts`). - Produces: `TeamsService.createNewTeam(teamdto: CreateTeamDTO, userId: string): Promise` now also creates a `Player` row linking the calling user to the new team with `teamRole.id === 3`. No signature change. - [ ] **Step 1: Write the failing test** Add to `myteamwallet_backend/src/teams/teams.service.spec.ts` (new `describe` block, e.g. after the existing two blocks): ```ts describe('TeamsService#createNewTeam', () => { const repository = { create: jest.fn(), save: jest.fn() }; const playerRepository = { create: jest.fn(), save: jest.fn() }; const rolesRepository = { findOneBy: jest.fn() }; const settingsRepository = { create: jest.fn(), save: jest.fn() }; const usersRepository = { findOneBy: jest.fn() }; const logger = { info: jest.fn(), debug: jest.fn(), warn: jest.fn() }; let service: TeamsService; beforeEach(() => { jest.resetAllMocks(); service = new TeamsService( repository as any, playerRepository as any, {} as any, rolesRepository as any, settingsRepository as any, {} as any, logger as any, {} as any, usersRepository as any, ); }); it('creates the team, its default settings, and a captain membership for the creator', async () => { const savedTeam = { id: 7, name: '1. Herren' }; repository.create.mockReturnValue({ name: '1. Herren' }); repository.save.mockResolvedValue(savedTeam); settingsRepository.create.mockImplementation((s: unknown) => s); settingsRepository.save.mockResolvedValue([]); usersRepository.findOneBy.mockResolvedValue({ id: 42, firstName: 'Alex', lastName: 'Muster', }); rolesRepository.findOneBy.mockResolvedValue({ id: 3, name: 'captain' }); playerRepository.create.mockImplementation((p: unknown) => p); playerRepository.save.mockResolvedValue({}); const result = await service.createNewTeam({ name: '1. Herren' } as any, '42'); expect(result).toBe(savedTeam); expect(rolesRepository.findOneBy).toHaveBeenCalledWith({ id: 3 }); expect(usersRepository.findOneBy).toHaveBeenCalledWith({ id: 42 }); expect(playerRepository.create).toHaveBeenCalledWith( expect.objectContaining({ firstName: 'Alex', lastName: 'Muster', team: savedTeam, teamRole: { id: 3, name: 'captain' }, }), ); expect(playerRepository.save).toHaveBeenCalled(); }); }); ``` Also update the **two existing** `new TeamsService(...)` constructions in this file (they currently pass 8 positional args; the constructor will have 9 after Step 3). Both blocks are byte-for-byte identical: ```ts service = new TeamsService( repository as any, {} as any, {} as any, {} as any, {} as any, {} as any, { info: jest.fn(), debug: jest.fn(), warn: jest.fn() } as any, access as any, ); ``` Replace **both** occurrences (find-and-replace-all) with a 9th argument appended: ```ts service = new TeamsService( repository as any, {} as any, {} as any, {} as any, {} as any, {} as any, { info: jest.fn(), debug: jest.fn(), warn: jest.fn() } as any, access as any, {} as any, ); ``` - [ ] **Step 2: Run test to verify it fails** Run (from `myteamwallet_backend/`): `npx jest teams.service.spec.ts` Expected: FAIL — TypeScript compile error (`Expected 9 arguments, but got 8`) on the two pre-existing constructions until Step 1's replace-all is applied, and then a runtime FAIL on the new test (`playerRepository.create` not called) until Step 3 is done. - [ ] **Step 3: Implement** In `myteamwallet_backend/src/teams/teams.service.ts`, add two imports (near the existing entity imports): ```ts import { TeamRolesEnum } from 'src/team-roles/team-roles.enum'; import { User } from 'src/users/entities/user.entity'; ``` Extend the constructor by appending a new injected repository as the **last** parameter (do not reorder the existing ones — `teams.service.spec.ts` relies on their positions): ```ts @Injectable() export class TeamsService { constructor( @InjectRepository(Team) private repository: Repository, @InjectRepository(Player) private playerRepository: Repository, @InjectRepository(Transaction) private transactionsRepository: Repository, @InjectRepository(TeamRole) private rolesRepository: Repository, @InjectRepository(TeamSetting) private settingsRepository: Repository, @InjectRepository(TeamWalletTransaction) private teamWalletTransactionRepository: Repository, private logger: LoggingService, private access: TeamAccessService, @InjectRepository(User) private usersRepository: Repository, ) {} ``` (`User` is already registered in `TeamsModule`'s `TypeOrmModule.forFeature([...])` — see `myteamwallet_backend/src/teams/teams.module.ts:31` — so no module change is needed.) Replace `createNewTeam` and add a new private helper right after it: ```ts async createNewTeam(teamdto: CreateTeamDTO, userId: string) { const createTeam = this.repository.create(teamdto); const team = await this.repository.save(createTeam); await this.generateBasicTeamSettings(team); await this.addCreatorAsCaptain(team, userId); await this.logger.info({ event: 'team_create', details: `created team id: ${team.id}`, userId: Number(userId), }); return team; } private async addCreatorAsCaptain(team: Team, userId: string): Promise { const [user, captainRole] = await Promise.all([ this.usersRepository.findOneBy({ id: Number(userId) }), this.rolesRepository.findOneBy({ id: TeamRolesEnum.captain }), ]); const player = this.playerRepository.create({ firstName: user?.firstName ?? '', lastName: user?.lastName ?? '', team, teamRole: captainRole, user: user ?? undefined, }); await this.playerRepository.save(player); } ``` (`generateBasicTeamSettings` stays exactly as-is, right below.) - [ ] **Step 4: Run test to verify it passes** Run: `npx jest teams.service.spec.ts` Expected: PASS (all existing tests plus the new `createNewTeam` test). - [ ] **Step 5: Commit** ```bash git add myteamwallet_backend/src/teams/teams.service.ts myteamwallet_backend/src/teams/teams.service.spec.ts git commit -m "feat: make team creator a captain of the new team" ``` --- ### Task 3: `TeamsApi.createTeam` (frontend HTTP call) **Files:** - Modify: `myteamwallet_frontend_modern/src/app/core/team/teams-api.ts` - Modify: `myteamwallet_frontend_modern/src/app/core/team/teams-api.spec.ts` **Interfaces:** - Consumes: `Team` model (`myteamwallet_frontend_modern/src/app/models/team.model.ts`: `{ id, name, alias, balance, outstanding?, players?, settings? }`). - Produces: `export interface CreateTeamRequest { name: string }` and `TeamsApi.createTeam(request: CreateTeamRequest): Observable` — used by Task 5's dialog component. - [ ] **Step 1: Write the failing test** Add to `myteamwallet_frontend_modern/src/app/core/team/teams-api.spec.ts` (alongside the existing per-method tests, following the same `httpMock.expectOne(...).flush(...)` pattern already in that file): ```ts it('creates a team', () => { const request = { name: '1. Herren' }; service.createTeam(request).subscribe(); const req = httpMock.expectOne(`${environment.apiUrl}teams`); expect(req.request.method).toBe('POST'); expect(req.request.body).toEqual(request); req.flush({ id: 7, name: '1. Herren', alias: 'a', balance: 0 }); }); ``` - [ ] **Step 2: Run test to verify it fails** Run (from `myteamwallet_frontend_modern/`): `npm test` Expected: FAIL — `service.createTeam is not a function`. - [ ] **Step 3: Implement** In `myteamwallet_frontend_modern/src/app/core/team/teams-api.ts`, add the request interface next to `CreatePlayerRequest`: ```ts export interface CreateTeamRequest { name: string; } ``` Add the method to the `TeamsApi` class (alongside `createPlayer`): ```ts createTeam(request: CreateTeamRequest): Observable { return this.http.post(`${environment.apiUrl}teams`, request); } ``` - [ ] **Step 4: Run test to verify it passes** Run: `npm test` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add myteamwallet_frontend_modern/src/app/core/team/teams-api.ts myteamwallet_frontend_modern/src/app/core/team/teams-api.spec.ts git commit -m "feat: add TeamsApi.createTeam" ``` --- ### Task 4: `MyTeamsStore.refresh` (force-reload after creating a team) **Files:** - Modify: `myteamwallet_frontend_modern/src/app/core/team/my-teams-store.ts` - Modify: `myteamwallet_frontend_modern/src/app/core/team/my-teams-store.spec.ts` **Interfaces:** - Consumes: `TeamsApi.loadMyTeams(userId: number): Observable` (existing). - Produces: `MyTeamsStore.refresh(userId: number): void` — always refetches, unlike `ensureLoaded` which is a no-op once loaded for that user. Used by Task 6. - [ ] **Step 1: Write the failing test** Add to `myteamwallet_frontend_modern/src/app/core/team/my-teams-store.spec.ts`: ```ts it('refresh always reloads, even for a user id already loaded', () => { store.ensureLoaded(42); httpMock.expectOne(`${environment.apiUrl}users/42/teams`).flush([player]); store.refresh(42); expect(store.loading()).toBe(true); const secondPlayer = { ...player, id: 2, team: { id: 6, name: 'Team B' } }; httpMock.expectOne(`${environment.apiUrl}users/42/teams`).flush([player, secondPlayer]); expect(store.loading()).toBe(false); expect(store.players()).toEqual([player, secondPlayer]); }); ``` - [ ] **Step 2: Run test to verify it fails** Run (from `myteamwallet_frontend_modern/`): `npm test` Expected: FAIL — `store.refresh is not a function`. - [ ] **Step 3: Implement** Replace the body of `myteamwallet_frontend_modern/src/app/core/team/my-teams-store.ts` with: ```ts import { Injectable, inject, signal } from '@angular/core'; import { UserTeamMembership } from '../../models/user-directory.model'; import { TeamsApi } from './teams-api'; @Injectable({ providedIn: 'root' }) export class MyTeamsStore { private readonly teamsApi = inject(TeamsApi); private readonly playersSignal = signal([]); private readonly loadingSignal = signal(false); private readonly loadedForUserId = signal(null); readonly players = this.playersSignal.asReadonly(); readonly loading = this.loadingSignal.asReadonly(); ensureLoaded(userId: number): void { if (this.loadedForUserId() === userId || this.loadingSignal()) { return; } this.fetch(userId); } refresh(userId: number): void { this.fetch(userId); } private fetch(userId: number): void { this.loadingSignal.set(true); this.teamsApi.loadMyTeams(userId).subscribe({ next: (players) => { this.playersSignal.set(players); this.loadedForUserId.set(userId); this.loadingSignal.set(false); }, error: () => { this.loadingSignal.set(false); }, }); } } ``` - [ ] **Step 4: Run test to verify it passes** Run: `npm test` Expected: PASS (all existing `MyTeamsStore` tests plus the new one). - [ ] **Step 5: Commit** ```bash git add myteamwallet_frontend_modern/src/app/core/team/my-teams-store.ts myteamwallet_frontend_modern/src/app/core/team/my-teams-store.spec.ts git commit -m "feat: add MyTeamsStore.refresh for forced reloads" ``` --- ### Task 5: `CreateTeamDialog` component **Files:** - Create: `myteamwallet_frontend_modern/src/app/features/team-select/create-team-dialog/create-team-dialog.ts` - Test: `myteamwallet_frontend_modern/src/app/features/team-select/create-team-dialog/create-team-dialog.spec.ts` **Interfaces:** - Consumes: `TeamsApi.createTeam` (Task 3); `MatDialogRef` (Angular Material). - Produces: `CreateTeamDialog` standalone component. Opening it via `MatDialog.open(CreateTeamDialog)` and subscribing to `afterClosed()` yields the created `Team` on success, or `undefined` if cancelled/dismissed. Used by Task 6. This introduces a new pattern for this codebase: the only existing `MatDialogRef`/`MAT_DIALOG_DATA`-injecting component is `shared/confirm-dialog/confirm-dialog.ts` (a plain confirm/cancel dialog, no form). This component follows the same injection style but uses a `ReactiveFormsModule` form, matching how `members.ts`/`penalties.ts` build their (inline, non-dialog) create forms. - [ ] **Step 1: Write the failing test** Create `myteamwallet_frontend_modern/src/app/features/team-select/create-team-dialog/create-team-dialog.spec.ts`: ```ts import { TestBed } from '@angular/core/testing'; import { provideHttpClient } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { MatDialogRef } from '@angular/material/dialog'; import { CreateTeamDialog } from './create-team-dialog'; import { environment } from '../../../../environments/environment'; describe('CreateTeamDialog', () => { let dialogRef: { close: ReturnType }; let httpMock: HttpTestingController; beforeEach(async () => { dialogRef = { close: vi.fn() }; await TestBed.configureTestingModule({ imports: [CreateTeamDialog], providers: [ provideHttpClient(), provideHttpClientTesting(), { provide: MatDialogRef, useValue: dialogRef }, ], }).compileComponents(); httpMock = TestBed.inject(HttpTestingController); }); afterEach(() => { httpMock.verify(); }); it('keeps the submit button disabled until a team name is entered', () => { const fixture = TestBed.createComponent(CreateTeamDialog); fixture.detectChanges(); expect(fixture.componentInstance['form'].invalid).toBe(true); fixture.componentInstance['form'].controls.name.setValue('1. Herren'); expect(fixture.componentInstance['form'].invalid).toBe(false); }); it('creates the team and closes the dialog with the created team', () => { const fixture = TestBed.createComponent(CreateTeamDialog); fixture.detectChanges(); fixture.componentInstance['form'].controls.name.setValue('1. Herren'); fixture.componentInstance['submit'](); const request = httpMock.expectOne(`${environment.apiUrl}teams`); expect(request.request.body).toEqual({ name: '1. Herren' }); request.flush({ id: 9, name: '1. Herren', alias: 'a', balance: 0 }); expect(dialogRef.close).toHaveBeenCalledWith({ id: 9, name: '1. Herren', alias: 'a', balance: 0, }); }); it('re-enables the form and keeps the dialog open when the request fails', () => { const fixture = TestBed.createComponent(CreateTeamDialog); fixture.detectChanges(); fixture.componentInstance['form'].controls.name.setValue('1. Herren'); fixture.componentInstance['submit'](); httpMock .expectOne(`${environment.apiUrl}teams`) .flush('error', { status: 500, statusText: 'Server Error' }); expect(dialogRef.close).not.toHaveBeenCalled(); expect(fixture.componentInstance['saving']()).toBe(false); }); }); ``` - [ ] **Step 2: Run test to verify it fails** Run (from `myteamwallet_frontend_modern/`): `npm test` Expected: FAIL — `create-team-dialog` module not found. - [ ] **Step 3: Implement** Create `myteamwallet_frontend_modern/src/app/features/team-select/create-team-dialog/create-team-dialog.ts`: ```ts import { Component, inject, signal } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { Team } from '../../../models/team.model'; import { TeamsApi } from '../../../core/team/teams-api'; @Component({ selector: 'app-create-team-dialog', imports: [ReactiveFormsModule, MatButtonModule, MatDialogModule, MatFormFieldModule, MatInputModule], template: `

Team erstellen

Teamname
`, styles: [ ` .full-width { width: 100%; } `, ], }) export class CreateTeamDialog { protected readonly dialogRef = inject(MatDialogRef); private readonly formBuilder = inject(FormBuilder); private readonly teamsApi = inject(TeamsApi); protected readonly saving = signal(false); protected readonly form = this.formBuilder.nonNullable.group({ name: ['', Validators.required], }); protected submit(): void { if (this.form.invalid || this.saving()) return; this.saving.set(true); this.teamsApi.createTeam(this.form.getRawValue()).subscribe({ next: (team) => { this.saving.set(false); this.dialogRef.close(team); }, error: () => this.saving.set(false), }); } } ``` - [ ] **Step 4: Run test to verify it passes** Run: `npm test` Expected: PASS (3 new tests). - [ ] **Step 5: Commit** ```bash git add myteamwallet_frontend_modern/src/app/features/team-select/create-team-dialog/ git commit -m "feat: add CreateTeamDialog component" ``` --- ### Task 6: Wire "Team erstellen" into `team-select` **Files:** - Modify: `myteamwallet_frontend_modern/src/app/features/team-select/team-select.ts` - Modify: `myteamwallet_frontend_modern/src/app/features/team-select/team-select.html` - Modify: `myteamwallet_frontend_modern/src/app/features/team-select/team-select.scss` - Modify: `myteamwallet_frontend_modern/src/app/features/team-select/team-select.spec.ts` **Interfaces:** - Consumes: `CreateTeamDialog` (Task 5); `MyTeamsStore.refresh(userId)` (Task 4); `MatDialog` (Angular Material). - Produces: nothing consumed by later tasks — this is the last task. - [ ] **Step 1: Write the failing test** Add to `myteamwallet_frontend_modern/src/app/features/team-select/team-select.spec.ts`. First, add these imports at the top of the file: ```ts import { Subject } from 'rxjs'; import { MatDialog } from '@angular/material/dialog'; import { Team } from '../../models/team.model'; import { MyTeamsStore } from '../../core/team/my-teams-store'; import { CreateTeamDialog } from './create-team-dialog/create-team-dialog'; ``` Then replace the `describe('TeamSelect', ...)` setup (`let` declarations through the closing of `beforeEach`/`afterEach`) with: ```ts describe('TeamSelect', () => { let httpMock: HttpTestingController; let router: Router; let authStore: AuthStore; let myTeamsStore: MyTeamsStore; let dialogClosed: Subject; let dialog: { open: ReturnType }; beforeEach(async () => { localStorage.clear(); dialogClosed = new Subject(); dialog = { open: vi.fn(() => ({ afterClosed: () => dialogClosed.asObservable() })) }; await TestBed.configureTestingModule({ imports: [TeamSelect], providers: [ provideHttpClient(), provideHttpClientTesting(), provideRouter([]), { provide: MatDialog, useValue: dialog }, ], }).compileComponents(); httpMock = TestBed.inject(HttpTestingController); router = TestBed.inject(Router); authStore = TestBed.inject(AuthStore); myTeamsStore = TestBed.inject(MyTeamsStore); authStore.setSession('token', { id: 42, email: 'a@b.de', firstName: 'A', lastName: 'B' }); }); afterEach(() => { httpMock.verify(); }); ``` (The existing three `it(...)` blocks stay unchanged below this.) Then add two new tests at the end of the `describe` block, before its closing `});`: ```ts it('opens the create-team dialog and navigates into the newly created team on success', async () => { const navigateSpy = vi.spyOn(router, 'navigate'); const refreshSpy = vi.spyOn(myTeamsStore, 'refresh'); const fixture = TestBed.createComponent(TeamSelect); fixture.detectChanges(); httpMock.expectOne(`${environment.apiUrl}users/42/teams`).flush([]); await fixture.whenStable(); fixture.detectChanges(); fixture.componentInstance['createTeam'](); expect(dialog.open).toHaveBeenCalledWith(CreateTeamDialog); const created: Team = { id: 9, name: '1. Herren', alias: 'a', balance: 0 }; dialogClosed.next(created); expect(refreshSpy).toHaveBeenCalledWith(42); expect(navigateSpy).toHaveBeenCalledWith(['/team', 9, 'overview']); httpMock.expectOne(`${environment.apiUrl}users/42/teams`).flush([]); }); it('does not navigate when the create-team dialog is dismissed without a team', async () => { const navigateSpy = vi.spyOn(router, 'navigate'); const fixture = TestBed.createComponent(TeamSelect); fixture.detectChanges(); httpMock.expectOne(`${environment.apiUrl}users/42/teams`).flush([]); await fixture.whenStable(); fixture.detectChanges(); fixture.componentInstance['createTeam'](); dialogClosed.next(undefined); expect(navigateSpy).not.toHaveBeenCalled(); }); ``` - [ ] **Step 2: Run test to verify it fails** Run (from `myteamwallet_frontend_modern/`): `npm test` Expected: FAIL — `createTeam` does not exist on `TeamSelect`, and `MatDialog` is unused/component doesn't inject it yet. - [ ] **Step 3: Implement** Replace `myteamwallet_frontend_modern/src/app/features/team-select/team-select.ts` with: ```ts import { Component, effect, inject } from '@angular/core'; import { Router, RouterLink } from '@angular/router'; import { MatButtonModule } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { AuthStore } from '../../core/auth/auth-store'; import { MyTeamsStore } from '../../core/team/my-teams-store'; import { Team } from '../../models/team.model'; import { CreateTeamDialog } from './create-team-dialog/create-team-dialog'; @Component({ selector: 'app-team-select', imports: [RouterLink, MatButtonModule, MatIconModule, MatListModule, MatProgressSpinnerModule], templateUrl: './team-select.html', styleUrl: './team-select.scss', }) export class TeamSelect { private readonly authStore = inject(AuthStore); private readonly myTeamsStore = inject(MyTeamsStore); private readonly router = inject(Router); private readonly dialog = inject(MatDialog); protected readonly players = this.myTeamsStore.players; protected readonly loading = this.myTeamsStore.loading; constructor() { const userId = this.authStore.currentUser()?.id; if (userId) { this.myTeamsStore.ensureLoaded(userId); } effect(() => { const players = this.myTeamsStore.players(); if (!this.myTeamsStore.loading() && players.length === 1 && players[0].team) { void this.router.navigate(['/team', players[0].team.id, 'overview'], { replaceUrl: true, }); } }); } protected createTeam(): void { this.dialog .open(CreateTeamDialog) .afterClosed() .subscribe((team: Team | undefined) => { if (!team) return; const userId = this.authStore.currentUser()?.id; if (userId) { this.myTeamsStore.refresh(userId); } void this.router.navigate(['/team', team.id, 'overview']); }); } } ``` Replace `myteamwallet_frontend_modern/src/app/features/team-select/team-select.html` with: ```html
@if (loading()) {
} @else if (players().length === 0) {

Du bist noch keinem Team zugeordnet.

} @else {

Team auswählen

@for (player of players(); track player.id) { {{ player.team.name }} {{ player.firstName }} {{ player.lastName }} }
} ``` Add to `myteamwallet_frontend_modern/src/app/features/team-select/team-select.scss`: ```scss .team-select-toolbar { display: flex; justify-content: flex-end; padding: 1rem 1rem 0; } ``` - [ ] **Step 4: Run test to verify it passes** Run: `npm test` Expected: PASS (all `TeamSelect` tests, including the two new ones). - [ ] **Step 5: Commit** ```bash git add myteamwallet_frontend_modern/src/app/features/team-select/ git commit -m "feat: wire create-team dialog into team-select" ``` --- ## Manual End-to-End Verification After all tasks are complete: 1. Start the backend (`npm run start:dev` in `myteamwallet_backend/`) and frontend (`npm start` in `myteamwallet_frontend_modern/`). 2. Log in as a regular (non-admin) user. 3. Go to the team selection screen, click "Team erstellen", enter a name, submit. 4. Confirm: the dialog closes, the app navigates to `/team//overview`, and the user appears under "Mitglieder" with role "Kapitän". 5. Go back to team selection (or log in as a user with 2+ teams) and confirm the "Team erstellen" button is visible even when teams already exist. 6. Optionally, call `POST /api/v1/teams` directly via Swagger as a non-admin user's JWT to confirm the 403 is gone.