From 0e8ac74116bda8aad813270ce872530fcfdfef18 Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Sun, 2 Aug 2026 09:22:16 +0200 Subject: [PATCH] feat: add manager guide and contextual help --- .../src/auth/dto/auth-update.dto.spec.ts | 24 +++ .../src/auth/dto/auth-update.dto.ts | 6 +- .../1785520800000-AddHelpTextsEnabled.ts | 17 +++ .../migrations/AddHelpTextsEnabled.spec.ts | 31 ++++ .../src/users/entities/user.entity.ts | 3 + .../src/app/app.config.ts | 10 +- .../src/app/app.routes.spec.ts | 5 + .../src/app/app.routes.ts | 5 + .../src/app/core/auth/auth-api.ts | 1 + .../src/app/core/help/guide-content.ts | 144 ++++++++++++++++++ .../src/app/core/help/help-access.spec.ts | 87 +++++++++++ .../src/app/core/help/help-access.ts | 32 ++++ .../app/features/team/cashbox/cashbox.html | 9 ++ .../app/features/team/cashbox/cashbox.spec.ts | 1 + .../src/app/features/team/cashbox/cashbox.ts | 2 + .../app/features/team/members/members.html | 8 + .../app/features/team/members/members.spec.ts | 1 + .../src/app/features/team/members/members.ts | 2 + .../app/features/team/more/guide/guide.html | 61 ++++++++ .../app/features/team/more/guide/guide.scss | 132 ++++++++++++++++ .../features/team/more/guide/guide.spec.ts | 53 +++++++ .../src/app/features/team/more/guide/guide.ts | 39 +++++ .../app/features/team/more/invite/invite.html | 9 ++ .../features/team/more/invite/invite.spec.ts | 1 + .../app/features/team/more/invite/invite.ts | 2 + .../src/app/features/team/more/more.html | 12 ++ .../src/app/features/team/more/more.spec.ts | 24 +++ .../src/app/features/team/more/more.ts | 3 + .../team/more/penalties/penalties.html | 9 ++ .../team/more/penalties/penalties.spec.ts | 1 + .../features/team/more/penalties/penalties.ts | 2 + .../features/team/more/profile/profile.html | 11 ++ .../features/team/more/profile/profile.scss | 13 ++ .../team/more/profile/profile.spec.ts | 55 ++++++- .../app/features/team/more/profile/profile.ts | 14 +- .../more/public-access/public-access.html | 9 ++ .../more/public-access/public-access.spec.ts | 2 + .../team/more/public-access/public-access.ts | 2 + .../src/app/models/user.model.ts | 1 + .../app/shared/context-help/context-help.html | 24 +++ .../app/shared/context-help/context-help.scss | 46 ++++++ .../shared/context-help/context-help.spec.ts | 62 ++++++++ .../app/shared/context-help/context-help.ts | 33 ++++ 43 files changed, 1002 insertions(+), 6 deletions(-) create mode 100644 myteamwallet_backend/src/auth/dto/auth-update.dto.spec.ts create mode 100644 myteamwallet_backend/src/database/migrations/1785520800000-AddHelpTextsEnabled.ts create mode 100644 myteamwallet_backend/src/database/migrations/AddHelpTextsEnabled.spec.ts create mode 100644 myteamwallet_frontend_modern/src/app/core/help/guide-content.ts create mode 100644 myteamwallet_frontend_modern/src/app/core/help/help-access.spec.ts create mode 100644 myteamwallet_frontend_modern/src/app/core/help/help-access.ts create mode 100644 myteamwallet_frontend_modern/src/app/features/team/more/guide/guide.html create mode 100644 myteamwallet_frontend_modern/src/app/features/team/more/guide/guide.scss create mode 100644 myteamwallet_frontend_modern/src/app/features/team/more/guide/guide.spec.ts create mode 100644 myteamwallet_frontend_modern/src/app/features/team/more/guide/guide.ts create mode 100644 myteamwallet_frontend_modern/src/app/shared/context-help/context-help.html create mode 100644 myteamwallet_frontend_modern/src/app/shared/context-help/context-help.scss create mode 100644 myteamwallet_frontend_modern/src/app/shared/context-help/context-help.spec.ts create mode 100644 myteamwallet_frontend_modern/src/app/shared/context-help/context-help.ts diff --git a/myteamwallet_backend/src/auth/dto/auth-update.dto.spec.ts b/myteamwallet_backend/src/auth/dto/auth-update.dto.spec.ts new file mode 100644 index 0000000..2c36cad --- /dev/null +++ b/myteamwallet_backend/src/auth/dto/auth-update.dto.spec.ts @@ -0,0 +1,24 @@ +import { validate } from 'class-validator'; +import { AuthUpdateDto } from './auth-update.dto'; + +describe('AuthUpdateDto help preference', () => { + it('rejects a non-boolean helpTextsEnabled value', async () => { + const dto = Object.assign(new AuthUpdateDto(), { + helpTextsEnabled: 'yes', + }); + + const errors = await validate(dto); + + expect(errors.some((error) => error.property === 'helpTextsEnabled')).toBe( + true, + ); + }); + + it('accepts a boolean helpTextsEnabled value without requiring profile fields', async () => { + const dto = Object.assign(new AuthUpdateDto(), { + helpTextsEnabled: false, + }); + + await expect(validate(dto)).resolves.toEqual([]); + }); +}); diff --git a/myteamwallet_backend/src/auth/dto/auth-update.dto.ts b/myteamwallet_backend/src/auth/dto/auth-update.dto.ts index a75dcdb..7299715 100644 --- a/myteamwallet_backend/src/auth/dto/auth-update.dto.ts +++ b/myteamwallet_backend/src/auth/dto/auth-update.dto.ts @@ -1,8 +1,12 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsOptional, MinLength, Validate } from 'class-validator'; +import { IsBoolean, IsNotEmpty, IsOptional, MinLength } from 'class-validator'; import { IsExist } from '../../utils/validators/is-exists.validator'; export class AuthUpdateDto { + @ApiProperty({ default: true }) + @IsOptional() + @IsBoolean() + helpTextsEnabled?: boolean; @ApiProperty({ example: 'John' }) @IsOptional() diff --git a/myteamwallet_backend/src/database/migrations/1785520800000-AddHelpTextsEnabled.ts b/myteamwallet_backend/src/database/migrations/1785520800000-AddHelpTextsEnabled.ts new file mode 100644 index 0000000..2e01082 --- /dev/null +++ b/myteamwallet_backend/src/database/migrations/1785520800000-AddHelpTextsEnabled.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddHelpTextsEnabled1785520800000 implements MigrationInterface { + name = 'AddHelpTextsEnabled1785520800000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'ALTER TABLE "user" ADD "helpTextsEnabled" boolean NOT NULL DEFAULT true', + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'ALTER TABLE "user" DROP COLUMN "helpTextsEnabled"', + ); + } +} diff --git a/myteamwallet_backend/src/database/migrations/AddHelpTextsEnabled.spec.ts b/myteamwallet_backend/src/database/migrations/AddHelpTextsEnabled.spec.ts new file mode 100644 index 0000000..acd9617 --- /dev/null +++ b/myteamwallet_backend/src/database/migrations/AddHelpTextsEnabled.spec.ts @@ -0,0 +1,31 @@ +import { getMetadataArgsStorage } from 'typeorm'; +import { User } from '../../users/entities/user.entity'; + +describe('AddHelpTextsEnabled1785520800000', () => { + it('adds a reversible enabled-by-default user preference', async () => { + const migrationModule = require('./1785520800000-AddHelpTextsEnabled'); + const migration = new migrationModule.AddHelpTextsEnabled1785520800000(); + const queryRunner = { query: jest.fn() } as any; + + await migration.up(queryRunner); + expect(queryRunner.query).toHaveBeenCalledWith( + 'ALTER TABLE "user" ADD "helpTextsEnabled" boolean NOT NULL DEFAULT true', + ); + + queryRunner.query.mockClear(); + await migration.down(queryRunner); + expect(queryRunner.query).toHaveBeenCalledWith( + 'ALTER TABLE "user" DROP COLUMN "helpTextsEnabled"', + ); + }); + + it('keeps the user entity default aligned with the migration', () => { + const column = getMetadataArgsStorage().columns.find( + (candidate) => + candidate.target === User && + candidate.propertyName === 'helpTextsEnabled', + ); + + expect(column?.options).toMatchObject({ type: Boolean, default: true }); + }); +}); diff --git a/myteamwallet_backend/src/users/entities/user.entity.ts b/myteamwallet_backend/src/users/entities/user.entity.ts index 455ef43..669d18b 100644 --- a/myteamwallet_backend/src/users/entities/user.entity.ts +++ b/myteamwallet_backend/src/users/entities/user.entity.ts @@ -65,6 +65,9 @@ export class User extends EntityHelper { @Column({ nullable: true }) lastName: string | null; + @Column({ default: true }) + helpTextsEnabled: boolean; + @ManyToOne(() => Role, { eager: true, }) diff --git a/myteamwallet_frontend_modern/src/app/app.config.ts b/myteamwallet_frontend_modern/src/app/app.config.ts index 7804b2d..e24a12a 100644 --- a/myteamwallet_frontend_modern/src/app/app.config.ts +++ b/myteamwallet_frontend_modern/src/app/app.config.ts @@ -1,5 +1,5 @@ import { ApplicationConfig, isDevMode, provideBrowserGlobalErrorListeners } from '@angular/core'; -import { provideRouter } from '@angular/router'; +import { provideRouter, withInMemoryScrolling } from '@angular/router'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { provideServiceWorker } from '@angular/service-worker'; @@ -10,7 +10,13 @@ import { errorInterceptor } from './core/http/error-interceptor'; export const appConfig: ApplicationConfig = { providers: [ provideBrowserGlobalErrorListeners(), - provideRouter(routes), + provideRouter( + routes, + withInMemoryScrolling({ + anchorScrolling: 'enabled', + scrollPositionRestoration: 'enabled', + }), + ), provideHttpClient(withInterceptors([authInterceptor, errorInterceptor])), provideServiceWorker('ngsw-worker.js', { enabled: !isDevMode(), diff --git a/myteamwallet_frontend_modern/src/app/app.routes.spec.ts b/myteamwallet_frontend_modern/src/app/app.routes.spec.ts index 99753e4..cb97fa4 100644 --- a/myteamwallet_frontend_modern/src/app/app.routes.spec.ts +++ b/myteamwallet_frontend_modern/src/app/app.routes.spec.ts @@ -66,6 +66,11 @@ describe('app routing', () => { expect(teamRoute?.children?.some((route) => route.path === 'more/public-access')).toBe(true); }); + it('registers the guide below the protected team shell', () => { + const teamRoute = routes.find((route) => route.path === 'team/:id'); + expect(teamRoute?.children?.some((route) => route.path === 'more/guide')).toBe(true); + }); + it('accepts the password reset URL generated by the backend', async () => { expect(routes.some((route) => route.path === 'password-change/:hash')).toBe(true); await router.navigateByUrl('/password-change/reset-hash'); diff --git a/myteamwallet_frontend_modern/src/app/app.routes.ts b/myteamwallet_frontend_modern/src/app/app.routes.ts index c1dc1ec..221f238 100644 --- a/myteamwallet_frontend_modern/src/app/app.routes.ts +++ b/myteamwallet_frontend_modern/src/app/app.routes.ts @@ -101,6 +101,11 @@ export const routes: Routes = [ loadComponent: () => import('./features/team/more/public-access/public-access').then((m) => m.PublicAccess), }, + { + path: 'more/guide', + loadComponent: () => + import('./features/team/more/guide/guide').then((m) => m.Guide), + }, ], }, { diff --git a/myteamwallet_frontend_modern/src/app/core/auth/auth-api.ts b/myteamwallet_frontend_modern/src/app/core/auth/auth-api.ts index 1e48a1d..e4dd6a9 100644 --- a/myteamwallet_frontend_modern/src/app/core/auth/auth-api.ts +++ b/myteamwallet_frontend_modern/src/app/core/auth/auth-api.ts @@ -30,6 +30,7 @@ export interface CreateInviteRequest extends InviteDetails {} export interface UpdateProfileRequest { firstName?: string; lastName?: string; + helpTextsEnabled?: boolean; oldPassword?: string; password?: string; } diff --git a/myteamwallet_frontend_modern/src/app/core/help/guide-content.ts b/myteamwallet_frontend_modern/src/app/core/help/guide-content.ts new file mode 100644 index 0000000..209a02b --- /dev/null +++ b/myteamwallet_frontend_modern/src/app/core/help/guide-content.ts @@ -0,0 +1,144 @@ +export type GuideSectionId = + | 'roles' + | 'handover' + | 'members' + | 'bookings' + | 'reversals' + | 'penalties' + | 'sharing' + | 'next-handover' + | 'faq'; + +export interface GuideSection { + id: GuideSectionId; + title: string; + intro: string; + points: readonly string[]; + note?: string; +} + +export const HANDOVER_CHECKLIST = [ + 'Prüfe, ob dein Benutzerkonto mit dem richtigen Mitglied und der richtigen Teamrolle verknüpft ist.', + 'Vergleiche Teamkasse und offene Mitgliedersalden mit den Unterlagen der bisherigen Verantwortung.', + 'Kontrolliere aktive und inaktive Mitglieder sowie die vergebenen Verantwortungsrollen.', + 'Lies bestehende Buchungen und Stornierungen, bevor du eine neue Buchung erfasst.', + 'Prüfe Strafenkatalog, offene Einladungen und die öffentliche Freigabe des Teams.', + 'Vereinbare, ab welchem Datum du neue Vorgänge in TeamWallet pflegst.', +] as const; + +export const ROLE_GUIDE = [ + { + role: '2. Kassenwart', + description: 'Kann Buchungen erfassen und nachvollziehen; organisatorische Verwaltung bleibt der Teamleitung vorbehalten.', + }, + { + role: 'Kapitän', + description: 'Kann Kasse, Mitglieder, Strafenkatalog, Einladungen und Freigaben verwalten.', + }, + { + role: 'Kassenwart', + description: 'Trägt die zentrale Verantwortung für Kassenstand, Buchungen, Salden und Übergabe.', + }, + { + role: 'Trainer', + description: 'Kann die organisatorischen Verwaltungsfunktionen einschließlich Kasse und Mitglieder nutzen.', + }, +] as const; + +export const GUIDE_SECTIONS: readonly GuideSection[] = [ + { + id: 'roles', + title: 'Deine Rolle und Berechtigungen', + intro: 'TeamWallet zeigt Aktionen passend zu deiner Rolle. Eine ausgeblendete Aktion ist deshalb meist kein Fehler.', + points: [ + 'Deine Teamrolle gehört zur Mitgliedschaft im aktuell ausgewählten Team.', + 'Bei mehreren Teams können Rolle und Berechtigungen je Team unterschiedlich sein.', + 'Mindestens ein aktiver Kassenwart muss im Team erhalten bleiben.', + ], + }, + { + id: 'handover', + title: 'Amtsübergabe prüfen', + intro: 'Beginne nicht mit einer Korrekturbuchung, bevor du den übergebenen Stand gemeinsam geprüft hast.', + points: [ + 'Vergleiche den angezeigten Kassenstand mit Bargeld, Bankkonto oder der bisherigen Kassenliste.', + 'Offene Beiträge sind die Summe der Salden aktiver Mitglieder und nicht automatisch Bargeld in der Kasse.', + 'Dokumentiere Abweichungen außerhalb der App und kläre zuerst ihren Ursprung.', + ], + note: 'Nutze keine künstliche Testbuchung. Eine echte fehlerhafte Buchung wird über eine Stornierung nachvollziehbar korrigiert.', + }, + { + id: 'members', + title: 'Mitglieder und Rollen verwalten', + intro: 'Mitglied, Benutzerkonto und Teamrolle sind getrennte Dinge: Eine Person wird zuerst als Mitglied geführt und kann danach eingeladen werden.', + points: [ + 'Lege ein Mitglied nur einmal an und wähle die Rolle passend zur tatsächlichen Verantwortung.', + 'Deaktiviere ausgetretene Mitglieder, statt ihre Historie zu löschen.', + 'Prüfe vor einem Rollenwechsel, dass ein aktiver Kassenwart im Team verbleibt.', + ], + }, + { + id: 'bookings', + title: 'Buchungen sicher erfassen', + intro: 'Mitgliederbuchungen verändern persönliche Salden; Teambuchungen erfassen direkte Einnahmen oder Ausgaben der Teamkasse.', + points: [ + 'Wähle zuerst die richtige Buchungsart und prüfe Betrag, Datum, Personen und Notiz vor dem Speichern.', + 'Bei mehreren Mitgliedern entscheidet „Betrag gleichmäßig verteilen“, ob der Gesamtbetrag geteilt oder je Person gebucht wird.', + 'Nutze eine eindeutige Notiz, damit ein Vorgang auch Monate später verständlich bleibt.', + 'Ein Strafenkatalogeintrag kann Betrag, Art und Notiz einer Mitgliederbuchung vorbereiten.', + ], + }, + { + id: 'reversals', + title: 'Fehler nachvollziehen und stornieren', + intro: 'Eine Stornierung bewahrt die Originalbuchung und erzeugt eine nachvollziehbare Gegenbewegung.', + points: [ + 'Kontrolliere im Journal zuerst Person, Betrag, Datum und Buchungsart.', + 'Storniere die falsche Mitgliederbuchung und erfasse anschließend den korrekten Vorgang neu.', + 'Vermeide ausgleichende Fantasiebuchungen, weil dadurch der Prüfpfad unklar wird.', + ], + }, + { + id: 'penalties', + title: 'Strafenkatalog verwenden', + intro: 'Der Strafenkatalog enthält wiederkehrende Regeln und Beträge, aber noch keine Forderung gegen ein bestimmtes Mitglied.', + points: [ + 'Formuliere Einträge kurz und eindeutig und vermeide doppelte Regeln.', + 'Änderungen am Katalog verändern bereits gebuchte Strafen nicht rückwirkend.', + 'Übernimm einen Eintrag in der Kasse und wähle dort erst die betroffenen Mitglieder.', + ], + }, + { + id: 'sharing', + title: 'Einladen und Team teilen', + intro: 'Einladungen und öffentliche Freigabe erfüllen unterschiedliche Zwecke.', + points: [ + 'Eine Einladung verknüpft ein persönliches Benutzerkonto mit einem bereits angelegten Mitglied.', + 'Die öffentliche Freigabe zeigt Teamstand, Salden, Strafenkatalog und Buchungsverläufe ohne Anmeldung.', + 'Aktiviere öffentliche Freigaben nur nach Abstimmung im Team und erneuere den Link, wenn er unerwünscht weitergegeben wurde.', + ], + }, + { + id: 'next-handover', + title: 'Verantwortung weitergeben', + intro: 'Eine gute Übergabe sorgt dafür, dass Kassenstand und Zuständigkeit zu einem eindeutigen Datum nachvollziehbar sind.', + points: [ + 'Stimme Kassenstand und offene Salden gemeinsam ab.', + 'Übertrage die Teamrolle und prüfe den Zugang der neuen verantwortlichen Person.', + 'Erkläre besondere offene Vorgänge, Freigaben und vereinbarte Buchungstexte.', + 'Entferne deine eigene Rolle erst, wenn mindestens ein aktiver Kassenwart verbleibt.', + ], + }, + { + id: 'faq', + title: 'Häufige Fragen', + intro: 'Diese Antworten helfen bei typischen Unsicherheiten im laufenden Betrieb.', + points: [ + 'Falsche Person oder falscher Betrag: Original stornieren und korrekt neu buchen.', + 'Kassenstand weicht ab: Journal und externe Unterlagen ab dem letzten bestätigten Stand vergleichen.', + 'Aktion fehlt: ausgewähltes Team, aktive Mitgliedschaft und Teamrolle prüfen.', + 'Einladung passt nicht: prüfen, ob das richtige Mitglied im richtigen Team ausgewählt wurde.', + 'Hilfetexte stören: im Profil deaktivieren; diese vollständige Anleitung bleibt erreichbar.', + ], + }, +] as const; diff --git a/myteamwallet_frontend_modern/src/app/core/help/help-access.spec.ts b/myteamwallet_frontend_modern/src/app/core/help/help-access.spec.ts new file mode 100644 index 0000000..ca82020 --- /dev/null +++ b/myteamwallet_frontend_modern/src/app/core/help/help-access.spec.ts @@ -0,0 +1,87 @@ +import { signal } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { AuthStore } from '../auth/auth-store'; +import { TeamStore } from '../team/team-store'; +import { HelpAccessService } from './help-access'; + +describe('HelpAccessService', () => { + it('allows an active second treasurer to open the guide', () => { + const access = createAccess( + { id: 7, role: { id: 2 }, helpTextsEnabled: true }, + teamWithPlayer(7, 2, true), + ); + + expect(access.canOpenGuide()).toBe(true); + expect(access.showContextHelp()).toBe(true); + }); + + it('does not treat an inactive responsible player as responsible', () => { + const access = createAccess( + { id: 7, role: { id: 2 }, helpTextsEnabled: true }, + teamWithPlayer(7, 4, false), + ); + + expect(access.canOpenGuide()).toBe(false); + expect(access.showContextHelp()).toBe(false); + }); + + it('keeps the guide available when contextual help is disabled', () => { + const access = createAccess( + { id: 7, role: { id: 2 }, helpTextsEnabled: false }, + teamWithPlayer(7, 3, true), + ); + + expect(access.canOpenGuide()).toBe(true); + expect(access.showContextHelp()).toBe(false); + }); + + it('defaults contextual help to enabled for stored users without the new field', () => { + const access = createAccess( + { id: 7, role: { id: 2 } }, + teamWithPlayer(7, 5, true), + ); + + expect(access.showContextHelp()).toBe(true); + }); + + it('allows global administrators without a team player assignment', () => { + const access = createAccess( + { id: 1, role: { id: 1 }, helpTextsEnabled: true }, + { id: 5, name: 'Team', alias: 'team', balance: 0, players: [] }, + ); + + expect(access.canOpenGuide()).toBe(true); + }); +}); + +function createAccess(user: any, team: any): any { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + HelpAccessService, + { provide: AuthStore, useValue: { currentUser: signal(user) } }, + { provide: TeamStore, useValue: { team: signal(team) } }, + ], + }); + return TestBed.inject(HelpAccessService); +} + +function teamWithPlayer(userId: number, roleId: number, active: boolean): any { + return { + id: 5, + name: 'Team', + alias: 'team', + balance: 0, + players: [ + { + id: 9, + firstName: 'Alex', + lastName: 'Muster', + balance: 0, + active, + teamRole: { id: roleId }, + user: { id: userId }, + }, + ], + }; +} diff --git a/myteamwallet_frontend_modern/src/app/core/help/help-access.ts b/myteamwallet_frontend_modern/src/app/core/help/help-access.ts new file mode 100644 index 0000000..51ab1d4 --- /dev/null +++ b/myteamwallet_frontend_modern/src/app/core/help/help-access.ts @@ -0,0 +1,32 @@ +import { Injectable, computed, inject } from '@angular/core'; +import { RoleId } from '../../models/role.model'; +import { TeamRole } from '../../models/team-role.model'; +import { AuthStore } from '../auth/auth-store'; +import { TeamStore } from '../team/team-store'; + +@Injectable({ providedIn: 'root' }) +export class HelpAccessService { + private readonly authStore = inject(AuthStore); + private readonly teamStore = inject(TeamStore); + + readonly canOpenGuide = computed(() => { + const user = this.authStore.currentUser(); + if (!user) return false; + if (user.role?.id === RoleId.Admin) return true; + + return ( + this.teamStore + .team() + ?.players?.some( + (player) => + player.active && + player.user?.id === user.id && + (player.teamRole?.id ?? 0) >= TeamRole.ScndTreasurer, + ) ?? false + ); + }); + + readonly showContextHelp = computed( + () => this.canOpenGuide() && this.authStore.currentUser()?.helpTextsEnabled !== false, + ); +} diff --git a/myteamwallet_frontend_modern/src/app/features/team/cashbox/cashbox.html b/myteamwallet_frontend_modern/src/app/features/team/cashbox/cashbox.html index 3b5cfa1..781ecd0 100644 --- a/myteamwallet_frontend_modern/src/app/features/team/cashbox/cashbox.html +++ b/myteamwallet_frontend_modern/src/app/features/team/cashbox/cashbox.html @@ -10,6 +10,15 @@ + + @if (canBook()) {
diff --git a/myteamwallet_frontend_modern/src/app/features/team/cashbox/cashbox.spec.ts b/myteamwallet_frontend_modern/src/app/features/team/cashbox/cashbox.spec.ts index 2bc4226..6d546cd 100644 --- a/myteamwallet_frontend_modern/src/app/features/team/cashbox/cashbox.spec.ts +++ b/myteamwallet_frontend_modern/src/app/features/team/cashbox/cashbox.spec.ts @@ -139,6 +139,7 @@ describe('Cashbox', () => { expect(fixture.nativeElement.textContent).toContain('Bea Test'); expect(fixture.nativeElement.textContent).toContain('-12,00'); expect(fixture.nativeElement.querySelector('[data-testid="player-booking"]')).not.toBeNull(); + expect(fixture.nativeElement.textContent).toContain('Buchungen verstehen'); }); it('submits cent-preserving split transactions for selected players', async () => { diff --git a/myteamwallet_frontend_modern/src/app/features/team/cashbox/cashbox.ts b/myteamwallet_frontend_modern/src/app/features/team/cashbox/cashbox.ts index 17facda..0ff347d 100644 --- a/myteamwallet_frontend_modern/src/app/features/team/cashbox/cashbox.ts +++ b/myteamwallet_frontend_modern/src/app/features/team/cashbox/cashbox.ts @@ -26,6 +26,7 @@ import { import { ConfirmDialog, ConfirmDialogData } from '../../../shared/confirm-dialog/confirm-dialog'; import { splitAmounts } from './transaction-calculation'; import { signedTransactionAmount } from '../../../models/transaction-amount'; +import { ContextHelp } from '../../../shared/context-help/context-help'; registerLocaleData(localeDe); @@ -46,6 +47,7 @@ const HIGH_AMOUNT_CONFIRM_THRESHOLD = 300; MatProgressSpinnerModule, MatSelectModule, MatSnackBarModule, + ContextHelp, ], providers: [{ provide: LOCALE_ID, useValue: 'de-DE' }], templateUrl: './cashbox.html', diff --git a/myteamwallet_frontend_modern/src/app/features/team/members/members.html b/myteamwallet_frontend_modern/src/app/features/team/members/members.html index 8654b01..4c9fdb7 100644 --- a/myteamwallet_frontend_modern/src/app/features/team/members/members.html +++ b/myteamwallet_frontend_modern/src/app/features/team/members/members.html @@ -10,6 +10,14 @@ } + @if (showCreateForm()) { { const fixture = TestBed.createComponent(Members); fixture.detectChanges(); expect(fixture.nativeElement.textContent).toContain('Bea Test'); + expect(fixture.nativeElement.textContent).toContain('Mitglieder verwalten'); fixture.componentInstance['showCreateForm'].set(true); fixture.componentInstance['createForm'].setValue({ diff --git a/myteamwallet_frontend_modern/src/app/features/team/members/members.ts b/myteamwallet_frontend_modern/src/app/features/team/members/members.ts index 368e62f..0fb1586 100644 --- a/myteamwallet_frontend_modern/src/app/features/team/members/members.ts +++ b/myteamwallet_frontend_modern/src/app/features/team/members/members.ts @@ -12,6 +12,7 @@ import { MatSelectModule } from '@angular/material/select'; import { AuthStore } from '../../../core/auth/auth-store'; import { TeamStore } from '../../../core/team/team-store'; import { TeamsApi } from '../../../core/team/teams-api'; +import { ContextHelp } from '../../../shared/context-help/context-help'; registerLocaleData(localeDe); @@ -27,6 +28,7 @@ registerLocaleData(localeDe); MatIconModule, MatInputModule, MatSelectModule, + ContextHelp, ], providers: [{ provide: LOCALE_ID, useValue: 'de-DE' }], templateUrl: './members.html', diff --git a/myteamwallet_frontend_modern/src/app/features/team/more/guide/guide.html b/myteamwallet_frontend_modern/src/app/features/team/more/guide/guide.html new file mode 100644 index 0000000..c8fc93e --- /dev/null +++ b/myteamwallet_frontend_modern/src/app/features/team/more/guide/guide.html @@ -0,0 +1,61 @@ +@if (canOpenGuide()) { +arrow_backMehr + +
+

Team verwalten

+

Anleitung für Verantwortliche

+

Ein praktischer Leitfaden für Kassenwarte, Kapitäne, Trainer und ihre Vertretung.

+
+ + + +

Schnellstart

+

Neu in der Verantwortung?

+

Gehe diese Punkte gemeinsam mit der bisherigen verantwortlichen Person durch.

+
    + @for (item of handoverChecklist; track item) { +
  1. {{ item }}
  2. + } +
+
+
+ + + +
+ @for (section of sections; track section.id) { +
+

{{ $index + 1 | number: '2.0' }}

+

{{ section.title }}

+

{{ section.intro }}

+ + @if (section.id === 'roles') { +
+ @for (role of roleGuide; track role.role) { +
+ {{ role.role }} + {{ role.description }} +
+ } +
+ } + +
    + @for (point of section.points; track point) { +
  • {{ point }}
  • + } +
+ @if (section.note) { +

Wichtig: {{ section.note }}

+ } +
+ } +
+} diff --git a/myteamwallet_frontend_modern/src/app/features/team/more/guide/guide.scss b/myteamwallet_frontend_modern/src/app/features/team/more/guide/guide.scss new file mode 100644 index 0000000..9885288 --- /dev/null +++ b/myteamwallet_frontend_modern/src/app/features/team/more/guide/guide.scss @@ -0,0 +1,132 @@ +:host { + display: block; + max-width: 960px; + margin: 0 auto; + padding: 28px; +} + +.guide-header { + margin: 1rem 0 2rem; +} + +h1, +h2, +p { + margin-top: 0; +} + +h1 { + max-width: 18ch; + font-size: clamp(2rem, 5vw, 3.5rem); + line-height: 1.05; + margin-bottom: 0.75rem; +} + +.eyebrow { + color: var(--mat-sys-primary); + font-size: 0.75rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; + margin-bottom: 0.4rem; +} + +.handover-card { + border-radius: 1.25rem; + background: var(--mat-sys-primary-container); + color: var(--mat-sys-on-primary-container); +} + +.handover-card mat-card-content { + padding: clamp(1.25rem, 4vw, 2rem); +} + +.handover-card ol { + display: grid; + gap: 0.75rem; + padding-left: 1.4rem; + margin-bottom: 0; +} + +.guide-nav { + margin: 2rem 0; + padding: 1.25rem; + border: 1px solid var(--mat-sys-outline-variant); + border-radius: 1rem; +} + +.guide-nav div { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.8rem; +} + +.guide-nav a { + padding: 0.45rem 0.7rem; + border-radius: 999px; + background: var(--mat-sys-surface-container); + color: var(--mat-sys-on-surface); + text-decoration: none; +} + +.guide-sections { + display: grid; + gap: 1rem; +} + +.guide-section { + scroll-margin-top: 1.5rem; + padding: clamp(1.25rem, 4vw, 2rem); + border: 1px solid var(--mat-sys-outline-variant); + border-radius: 1.25rem; + background: var(--mat-sys-surface-container-lowest); +} + +.guide-section__intro { + max-width: 72ch; + color: var(--mat-sys-on-surface-variant); + font-size: 1.05rem; +} + +.guide-section ul { + display: grid; + gap: 0.55rem; + padding-left: 1.2rem; +} + +.role-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.75rem; + margin: 1.25rem 0; +} + +.role-grid article { + display: grid; + gap: 0.35rem; + padding: 1rem; + border-radius: 0.9rem; + background: var(--mat-sys-surface-container); +} + +.role-grid span { + color: var(--mat-sys-on-surface-variant); +} + +.guide-note { + margin: 1rem 0 0; + padding: 1rem; + border-left: 4px solid var(--mat-sys-tertiary); + background: var(--mat-sys-tertiary-container); +} + +@media (max-width: 600px) { + :host { + padding: 20px 16px; + } + + .role-grid { + grid-template-columns: 1fr; + } +} diff --git a/myteamwallet_frontend_modern/src/app/features/team/more/guide/guide.spec.ts b/myteamwallet_frontend_modern/src/app/features/team/more/guide/guide.spec.ts new file mode 100644 index 0000000..05d45ae --- /dev/null +++ b/myteamwallet_frontend_modern/src/app/features/team/more/guide/guide.spec.ts @@ -0,0 +1,53 @@ +import { Component, signal } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { provideRouter, Router } from '@angular/router'; +import { HelpAccessService } from '../../../../core/help/help-access'; +import { TeamStore } from '../../../../core/team/team-store'; +import { Guide } from './guide'; + +@Component({ template: '' }) +class EmptyRoute {} + +describe('Guide', () => { + it('renders the handover path and all task-oriented chapters for responsible users', async () => { + const fixture = await createFixture(true); + const text = fixture.nativeElement.textContent; + + expect(text).toContain('Neu in der Verantwortung?'); + expect(text).toContain('Amtsübergabe prüfen'); + expect(text).toContain('Buchungen sicher erfassen'); + expect(text).toContain('Verantwortung weitergeben'); + expect(fixture.nativeElement.querySelector('#bookings')).not.toBeNull(); + expect(fixture.nativeElement.querySelector('#faq')).not.toBeNull(); + }); + + it('redirects regular players back to the More page after team loading', async () => { + const fixture = await createFixture(false); + const router = TestBed.inject(Router); + await fixture.whenStable(); + + expect(router.url).toBe('/team/5/more'); + expect(fixture.nativeElement.textContent).not.toContain('Amtsübergabe prüfen'); + }); +}); + +async function createFixture(canOpen: boolean) { + await TestBed.configureTestingModule({ + imports: [Guide], + providers: [ + provideRouter([{ path: 'team/5/more', component: EmptyRoute }]), + { provide: HelpAccessService, useValue: { canOpenGuide: signal(canOpen) } }, + { + provide: TeamStore, + useValue: { + team: signal({ id: 5 }), + loading: signal(false), + }, + }, + ], + }).compileComponents(); + const fixture = TestBed.createComponent(Guide); + fixture.detectChanges(); + await fixture.whenStable(); + return fixture; +} diff --git a/myteamwallet_frontend_modern/src/app/features/team/more/guide/guide.ts b/myteamwallet_frontend_modern/src/app/features/team/more/guide/guide.ts new file mode 100644 index 0000000..85d7c3b --- /dev/null +++ b/myteamwallet_frontend_modern/src/app/features/team/more/guide/guide.ts @@ -0,0 +1,39 @@ +import { Component, effect, inject } from '@angular/core'; +import { DecimalPipe } from '@angular/common'; +import { Router, RouterLink } from '@angular/router'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatIconModule } from '@angular/material/icon'; +import { + GUIDE_SECTIONS, + HANDOVER_CHECKLIST, + ROLE_GUIDE, +} from '../../../../core/help/guide-content'; +import { HelpAccessService } from '../../../../core/help/help-access'; +import { TeamStore } from '../../../../core/team/team-store'; + +@Component({ + selector: 'app-guide', + imports: [DecimalPipe, RouterLink, MatButtonModule, MatCardModule, MatIconModule], + templateUrl: './guide.html', + styleUrl: './guide.scss', +}) +export class Guide { + private readonly helpAccess = inject(HelpAccessService); + private readonly router = inject(Router); + private readonly teamStore = inject(TeamStore); + + protected readonly sections = GUIDE_SECTIONS; + protected readonly handoverChecklist = HANDOVER_CHECKLIST; + protected readonly roleGuide = ROLE_GUIDE; + protected readonly canOpenGuide = this.helpAccess.canOpenGuide; + + constructor() { + effect(() => { + const team = this.teamStore.team(); + if (!this.teamStore.loading() && team && !this.helpAccess.canOpenGuide()) { + void this.router.navigateByUrl(`/team/${team.id}/more`); + } + }); + } +} diff --git a/myteamwallet_frontend_modern/src/app/features/team/more/invite/invite.html b/myteamwallet_frontend_modern/src/app/features/team/more/invite/invite.html index 4f2ff2a..67d1fa8 100644 --- a/myteamwallet_frontend_modern/src/app/features/team/more/invite/invite.html +++ b/myteamwallet_frontend_modern/src/app/features/team/more/invite/invite.html @@ -4,6 +4,15 @@

Einladen

Der Link ist personalisiert und darf nur an die ausgewählte Person gehen.

+ + @if (!canInvite()) { lock { }).compileComponents(); const fixture = TestBed.createComponent(Invite); fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toContain('Einladungen richtig zuordnen'); fixture.componentInstance['form'].setValue({ playerId: 7 }); fixture.componentInstance['generateLink'](); diff --git a/myteamwallet_frontend_modern/src/app/features/team/more/invite/invite.ts b/myteamwallet_frontend_modern/src/app/features/team/more/invite/invite.ts index c1418dc..22c5676 100644 --- a/myteamwallet_frontend_modern/src/app/features/team/more/invite/invite.ts +++ b/myteamwallet_frontend_modern/src/app/features/team/more/invite/invite.ts @@ -11,6 +11,7 @@ import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { AuthApi } from '../../../../core/auth/auth-api'; import { AuthStore } from '../../../../core/auth/auth-store'; import { TeamStore } from '../../../../core/team/team-store'; +import { ContextHelp } from '../../../../shared/context-help/context-help'; @Component({ selector: 'app-invite', @@ -24,6 +25,7 @@ import { TeamStore } from '../../../../core/team/team-store'; MatInputModule, MatSelectModule, MatSnackBarModule, + ContextHelp, ], templateUrl: './invite.html', styleUrl: './invite.scss', diff --git a/myteamwallet_frontend_modern/src/app/features/team/more/more.html b/myteamwallet_frontend_modern/src/app/features/team/more/more.html index 5ae851c..89c8864 100644 --- a/myteamwallet_frontend_modern/src/app/features/team/more/more.html +++ b/myteamwallet_frontend_modern/src/app/features/team/more/more.html @@ -5,6 +5,18 @@