diff --git a/alfa-client/apps/admin/src/index.html b/alfa-client/apps/admin/src/index.html index b1d42def4c92709d7794e04aaa3484164a21b4a8..e9b77e77d1493dd0f7e67f051941b5c48fff1b33 100644 --- a/alfa-client/apps/admin/src/index.html +++ b/alfa-client/apps/admin/src/index.html @@ -1,5 +1,5 @@ <!doctype html> -<html lang="en" class="h-full bg-white antialiased"> +<html lang="de" class="h-full bg-white antialiased"> <head> <meta charset="utf-8" /> <title>admin</title> @@ -7,7 +7,9 @@ <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" type="image/x-icon" href="favicon.ico" /> </head> - <body class="flex max-h-full min-h-full bg-white text-black dark:bg-slate-100 dark:bg-slate-900"> + <body + class="flex max-h-full min-h-full overflow-hidden bg-white text-black dark:bg-slate-900 dark:text-slate-100" + > <app-root class="flex w-full flex-col"></app-root> </body> </html> diff --git a/alfa-client/libs/admin/settings/src/lib/admin-settings.module.ts b/alfa-client/libs/admin/settings/src/lib/admin-settings.module.ts index 97761b18995e77099b9ce0f72361635ce395950d..40254fba8ff106cd0ff5ad7f6c9c471e5352c9e0 100644 --- a/alfa-client/libs/admin/settings/src/lib/admin-settings.module.ts +++ b/alfa-client/libs/admin/settings/src/lib/admin-settings.module.ts @@ -7,7 +7,7 @@ import { ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import KcAdminClient from '@keycloak/keycloak-admin-client'; import { ButtonWithSpinnerComponent, TextareaEditorComponent } from '@ods/component'; -import { TextInputComponent } from '@ods/system'; +import { MailboxIconComponent, PersonIconComponent, TextInputComponent } from '@ods/system'; import { createSettingListResourceService, SettingListResourceService, @@ -36,6 +36,7 @@ import { PrimaryButtonComponent } from './shared/primary-button/primary-button.c import { SecondaryButtonComponent } from './shared/secondary-button/secondary-button.component'; import { SpinnerComponent } from './shared/spinner/spinner.component'; import { TextFieldComponent } from './shared/text-field/text-field.component'; +import { UserNamePipe } from './user/user.util'; import { UsersRolesComponent } from './users-roles/users-roles.component'; @NgModule({ @@ -63,6 +64,9 @@ import { UsersRolesComponent } from './users-roles/users-roles.component'; TextInputComponent, ButtonWithSpinnerComponent, TextareaEditorComponent, + MailboxIconComponent, + PersonIconComponent, + UserNamePipe, ], exports: [ PostfachContainerComponent, diff --git a/alfa-client/libs/admin/settings/src/lib/user/user.util.spec.ts b/alfa-client/libs/admin/settings/src/lib/user/user.util.spec.ts index 7c0a60daa8b3f7e03fc2b974ada2d7b4ae2f4d13..f81178df11c54532bf0a00bbc1de516365f5339d 100644 --- a/alfa-client/libs/admin/settings/src/lib/user/user.util.spec.ts +++ b/alfa-client/libs/admin/settings/src/lib/user/user.util.spec.ts @@ -1,22 +1,47 @@ +import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation'; import { createOrganisationseinheitError } from '../../../test/user/user'; import { OrganisationseinheitError, OrganisationseinheitErrorType } from './user.model'; -import { KEYCLOAK_ERROR_MESSAGES, getOrganisationseinheitErrorMessage } from './user.util'; +import { + KEYCLOAK_ERROR_MESSAGES, + UserNamePipe, + getOrganisationseinheitErrorMessage, +} from './user.util'; -describe('get organisationseinheit error message', () => { - it('should map known error message', () => { - const nameConflictError: OrganisationseinheitError = createOrganisationseinheitError( - OrganisationseinheitErrorType.NAME_CONFLICT, - ); - const expectedMessage: string = KEYCLOAK_ERROR_MESSAGES[nameConflictError.errorType]; +describe('user util', () => { + describe('get organisationseinheit error message', () => { + it('should map known error message', () => { + const nameConflictError: OrganisationseinheitError = createOrganisationseinheitError( + OrganisationseinheitErrorType.NAME_CONFLICT, + ); + const expectedMessage: string = KEYCLOAK_ERROR_MESSAGES[nameConflictError.errorType]; - const message: string = getOrganisationseinheitErrorMessage(nameConflictError); - expect(message).toEqual(expectedMessage); + const message: string = getOrganisationseinheitErrorMessage(nameConflictError); + expect(message).toEqual(expectedMessage); + }); + + it('should map unknown error message to empty string', () => { + const nameConflictError: OrganisationseinheitError = createOrganisationseinheitError(null); + + const message: string = getOrganisationseinheitErrorMessage(nameConflictError); + expect(message).toEqual(''); + }); }); - it('should map unknown error message to empty string', () => { - const nameConflictError: OrganisationseinheitError = createOrganisationseinheitError(null); + describe('UserNamePipe', () => { + it('should return user name', () => { + const user: UserRepresentation = { firstName: 'Max', lastName: 'Mustermann' }; + const userNamePipe = new UserNamePipe(); + + const result: string = userNamePipe.transform(user); + expect(result).toBe('Max Mustermann'); + }); + + it('should return unknown user', () => { + const emptyUser: UserRepresentation = {}; + const userNamePipe = new UserNamePipe(); - const message: string = getOrganisationseinheitErrorMessage(nameConflictError); - expect(message).toEqual(''); + const result: string = userNamePipe.transform(emptyUser); + expect(result).toBe('Unbekannter Benutzer'); + }); }); }); diff --git a/alfa-client/libs/admin/settings/src/lib/user/user.util.ts b/alfa-client/libs/admin/settings/src/lib/user/user.util.ts index 2a1da4ffbdac3353761518988c9c112cbc450ca1..879b9e0365d8532a9b8c2dcea3513eae8d42a90c 100644 --- a/alfa-client/libs/admin/settings/src/lib/user/user.util.ts +++ b/alfa-client/libs/admin/settings/src/lib/user/user.util.ts @@ -1,3 +1,5 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation'; import { OrganisationseinheitError, OrganisationseinheitErrorType } from './user.model'; export const KEYCLOAK_ERROR_MESSAGES: { [type: string]: string } = { @@ -17,3 +19,14 @@ export const KEYCLOAK_CREATE_GROUPS_ERROR_STATUS: { export function getOrganisationseinheitErrorMessage(error: OrganisationseinheitError): string { return KEYCLOAK_ERROR_MESSAGES[error.errorType] ?? ''; } + +@Pipe({ + name: 'userName', + standalone: true, +}) +export class UserNamePipe implements PipeTransform { + transform(user: UserRepresentation): string { + if (!user.firstName || !user.lastName) return 'Unbekannter Benutzer'; + return `${user.firstName} ${user.lastName}`; + } +} diff --git a/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.html b/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.html index 96bab232f6ade64a1f96eff377da82bc77922195..028644f62dd76c7acc1b0d776bd3878961bcfb0d 100644 --- a/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.html +++ b/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.html @@ -1,9 +1,63 @@ <h1 class="heading-1">Benutzer & Rollen</h1> <ods-button-with-spinner text="Benutzer hinzufügen" class="py-8" dataTestId="add-user-button" /> <ng-container *ngIf="users$ | async as users"> - <ul> + <ul + class="divide-y divide-gray-300 rounded-md bg-background-50 text-text shadow-sm ring-1 ring-gray-300 empty:hidden" + > <li *ngFor="let user of users.resource"> - {{ user.firstName }} + <a + href="#" + class="flex flex-col items-start justify-between gap-6 border-primary-600/50 px-6 py-4 hover:bg-background-150 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus lg:flex-row" + > + <div class="flex-1 basis-1/2"> + <div class="mb-2 flex flex-wrap items-center gap-3"> + <h3 class="text-md font-semibold">{{ user | userName }}</h3> + <dl class="flex flex-wrap gap-2"> + <dt class="sr-only">Rollen:</dt> + <dd + *ngFor="let role of user.roles" + class="inline-flex flex-shrink-0 items-center rounded-full bg-green-50 px-1.5 py-0.5 text-sm font-medium text-green-700 ring-1 ring-inset ring-green-600/20" + > + {{ role }} + </dd> + </dl> + </div> + + <dl> + <div *ngIf="user.email" class="flex items-center gap-2"> + <dt> + <span class="sr-only">E-Mail:</span> + <ods-mailbox-icon size="small" class="stroke-gray-600" /> + </dt> + <dd>{{ user.email }}</dd> + </div> + <div class="flex items-center gap-2"> + <dt> + <span class="sr-only">Benutzername:</span> + <ods-person-icon /> + </dt> + <dd>{{ user.username }}</dd> + </div> + </dl> + </div> + + <div class="flex-1 basis-1/2"> + <h4 class="sr-only">Zuständige Stellen</h4> + <ng-container *ngIf="getAuthoritiesForUser(user) as authorities"> + <ul class="list-outside list-disc pl-4"> + <ng-container *ngFor="let authority of authorities.value"> + <li>{{ authority }}</li> + </ng-container> + </ul> + <p + *ngIf="getAuthoritiesText(authorities) as authoritiesText" + class="pl-4 text-gray-500" + > + {{ authoritiesText }} + </p> + </ng-container> + </div> + </a> </li> </ul> </ng-container> diff --git a/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.scss b/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.scss deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.spec.ts b/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.spec.ts index 403255f195b65cb354ab21c234e27adc1bd06c34..b36e2595abff4fb6519ba344aedd3b4e448a4336 100644 --- a/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.spec.ts +++ b/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.spec.ts @@ -1,9 +1,11 @@ import { Mock, mock } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation'; import { ButtonWithSpinnerComponent } from '@ods/component'; +import { MailboxIconComponent, PersonIconComponent } from '@ods/system'; import { MockComponent } from 'ng-mocks'; import { UserAndRolesService } from './userAndRolesService'; -import { UsersRolesComponent } from './users-roles.component'; +import { Groups, UsersRolesComponent } from './users-roles.component'; describe('UsersRolesComponent', () => { let component: UsersRolesComponent; @@ -17,8 +19,12 @@ describe('UsersRolesComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [UsersRolesComponent], - imports: [MockComponent(ButtonWithSpinnerComponent)], providers: [{ provide: UserAndRolesService, useValue: userAndRolesService }], + imports: [ + MockComponent(ButtonWithSpinnerComponent), + MockComponent(MailboxIconComponent), + MockComponent(PersonIconComponent), + ], }).compileComponents(); fixture = TestBed.createComponent(UsersRolesComponent); @@ -29,4 +35,66 @@ describe('UsersRolesComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + describe('component', () => { + describe('getAuthoritiesForUser', () => { + describe('value', () => { + it('should contain trimmed array', () => { + const user: UserRepresentation = { + groups: ['Test1', 'Test2', 'Test3', 'Test4'], + }; + + const result: Groups = component.getAuthoritiesForUser(user, 3); + + expect(result.value.length).toBe(3); + }); + }); + describe('rest', () => { + it('should contain count of rest elements', () => { + const user: UserRepresentation = { + groups: ['Test1', 'Test2', 'Test3', 'Test4'], + }; + + const result: Groups = component.getAuthoritiesForUser(user, 3); + + expect(result.rest).toBe(1); + }); + + it('should contain 0 if there are less authorities', () => { + const user: UserRepresentation = { + groups: ['Test1'], + }; + + const result: Groups = component.getAuthoritiesForUser(user, 3); + + expect(result.rest).toBe(0); + }); + }); + }); + describe('getAuthoritiesText', () => { + it('should return empty text', () => { + const result: string = component.getAuthoritiesText({ value: [], rest: 0 }); + + expect(result).toBe('keine zuständigen Stellen zugewiesen'); + }); + + it('should return count text', () => { + const result: string = component.getAuthoritiesText({ + value: ['Test1'], + rest: 2, + }); + + expect(result).toBe(`und 2 weitere`); + }); + + it('should return empty string', () => { + const result: string = component.getAuthoritiesText({ + value: ['Test1'], + rest: 0, + }); + + expect(result).toBe(``); + }); + }); + }); }); diff --git a/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.ts b/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.ts index 76c08323aa090b333d240aa51ef22786daf121ae..5c8a683ae3a6113e2278fa03cd3f1d325bc3447c 100644 --- a/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.ts +++ b/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.ts @@ -4,10 +4,16 @@ import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRep import { Observable } from 'rxjs'; import { UserAndRolesService } from './userAndRolesService'; +export const GROUPS_TO_SHOW = 3; + +export interface Groups { + value: string[]; // Trimmed array of groups + rest: number; // The number of rest groups +} + @Component({ selector: 'admin-users-roles', templateUrl: './users-roles.component.html', - styleUrl: './users-roles.component.scss', }) export class UsersRolesComponent { users$: Observable<StateResource<UserRepresentation[]>>; @@ -15,4 +21,19 @@ export class UsersRolesComponent { constructor(private userAndRolesService: UserAndRolesService) { this.users$ = this.userAndRolesService.get(); } + + getAuthoritiesForUser(user: UserRepresentation, groupsToShow: number = GROUPS_TO_SHOW): Groups { + const value = user.groups.slice(0, groupsToShow); + const groupsLengthDiff = user.groups.length - groupsToShow; + return { + value, + rest: groupsLengthDiff > 0 ? groupsLengthDiff : 0, + }; + } + + getAuthoritiesText(groups: Groups): string { + if (!groups.value.length) return 'keine zuständigen Stellen zugewiesen'; + if (groups.rest) return `und ${groups.rest} weitere`; + return ''; + } } diff --git a/alfa-client/libs/design-system/src/lib/icons/mailbox-icon/mailbox-icon.component.ts b/alfa-client/libs/design-system/src/lib/icons/mailbox-icon/mailbox-icon.component.ts index 84f8f446e56ed21aa7d1dfd2119a673b850897e9..07b28d630504aa66112c1af5fe9c030a273f1533 100644 --- a/alfa-client/libs/design-system/src/lib/icons/mailbox-icon/mailbox-icon.component.ts +++ b/alfa-client/libs/design-system/src/lib/icons/mailbox-icon/mailbox-icon.component.ts @@ -15,14 +15,12 @@ import { IconVariants, iconVariants } from '../iconVariants'; > <path d="M20 4H4C2.89543 4 2 4.89543 2 6V18C2 19.1046 2.89543 20 4 20H20C21.1046 20 22 19.1046 22 18V6C22 4.89543 21.1046 4 20 4Z" - stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <path d="M22 7L13.03 12.7C12.7213 12.8934 12.3643 12.996 12 12.996C11.6357 12.996 11.2787 12.8934 10.97 12.7L2 7" - stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"