diff --git a/client/angular.json b/client/angular.json index 445c723da6..c00b3a0ccd 100644 --- a/client/angular.json +++ b/client/angular.json @@ -55,7 +55,7 @@ { "type": "initial", "maximumWarning": "1500kb", - "maximumError": "3mb" + "maximumError": "3500kb" }, { "type": "anyComponentStyle", diff --git a/client/src/app/domain/models/poll/poll-constants.ts b/client/src/app/domain/models/poll/poll-constants.ts index 7e7e4ff86f..053e695478 100644 --- a/client/src/app/domain/models/poll/poll-constants.ts +++ b/client/src/app/domain/models/poll/poll-constants.ts @@ -102,6 +102,8 @@ export interface EntitledUsersEntry { present: boolean; voted: boolean; vote_delegated_to_user_id?: number; + user_merged_into_id?: number; + delegation_user_merged_into_id?: number; } export const VOTE_MAJORITY = -1; diff --git a/client/src/app/gateways/presenter/history-presenter.service.ts b/client/src/app/gateways/presenter/history-presenter.service.ts index 69ea7769ce..9f6930a141 100644 --- a/client/src/app/gateways/presenter/history-presenter.service.ts +++ b/client/src/app/gateways/presenter/history-presenter.service.ts @@ -70,13 +70,14 @@ export class HistoryPresenterService { .flatMap(positions => getUniqueItems(positions)) .sort((positionA, positionB) => positionB.timestamp - positionA.timestamp) .map(position => { + const userView = this.userRepo.getViewModel(position.user_id); return new HistoryPosition({ ...position, information: Array.isArray(position.information) ? position.information : position?.information[fqid], fqid, - user: this.userRepo.getViewModel(position.user_id)?.getFullName() + user: userView?.getFullName() ? userView.getFullName() : `user/${position.user_id}` }); }); } diff --git a/client/src/app/gateways/repositories/users/user-repository.service.ts b/client/src/app/gateways/repositories/users/user-repository.service.ts index 413bce9313..a27bcd93c1 100644 --- a/client/src/app/gateways/repositories/users/user-repository.service.ts +++ b/client/src/app/gateways/repositories/users/user-repository.service.ts @@ -508,6 +508,10 @@ export class UserRepositoryService extends BaseRepository { return this.createAction(UserAction.PARTICIPANT_IMPORT, payload); } + public mergeTogether(payload: { id: number; user_ids: number[] }[]): Action { + return this.createAction(UserAction.MERGE_TOGETHER, payload); + } + private sanitizePayload(payload: any): any { const temp = { ...payload }; for (const key of Object.keys(temp).filter(field => !this.isFieldAllowedToBeEmpty(field))) { diff --git a/client/src/app/site/pages/meetings/modules/poll/base/base-poll-detail.component.ts b/client/src/app/site/pages/meetings/modules/poll/base/base-poll-detail.component.ts index d0ad1600df..8ecf46cdbe 100644 --- a/client/src/app/site/pages/meetings/modules/poll/base/base-poll-detail.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/base/base-poll-detail.component.ts @@ -230,6 +230,12 @@ export abstract class BasePollDetailComponent user.id === entry.vote_delegated_to_user_id) + : null, + user_merged_into: entry.user_merged_into_id + ? `${this.translate.instant(`Old account of`)} ${users + .find(user => user.id === entry.user_merged_into_id) + ?.getShortName()}` + : null, + delegation_user_merged_into: entry.delegation_user_merged_into_id + ? `(${this.translate.instant(`represented by old account of`)}) ${users + .find(user => user.id === entry.delegation_user_merged_into_id) + ?.getShortName()}` : null }); } diff --git a/client/src/app/site/pages/meetings/modules/poll/base/base-poll-pdf.service.ts b/client/src/app/site/pages/meetings/modules/poll/base/base-poll-pdf.service.ts index 3941ba1f23..9fd2c350f3 100644 --- a/client/src/app/site/pages/meetings/modules/poll/base/base-poll-pdf.service.ts +++ b/client/src/app/site/pages/meetings/modules/poll/base/base-poll-pdf.service.ts @@ -626,17 +626,26 @@ export abstract class BasePollPdfService { for (const date of usersData.sort((entryA, entryB) => entryA.user?.getName().localeCompare(entryB.user?.getName()) )) { + const name = date.user_merged_into_id + ? `${this.translate.instant(`Old account of`)} ` + + this.getUserNameForExport(this.userRepo.getViewModel(date.user_merged_into_id)) + : this.getUserNameForExport(date.user); + let represented = ``; + if (date.vote_delegated_to_user_id && !date.delegation_user_merged_into_id) { + represented = + `\n${this.translate.instant(`represented by`)} ` + + this.getUserNameForExport(date.vote_delegated_to); + } else if (date.vote_delegated_to_user_id && date.delegation_user_merged_into_id) { + represented = + `\n${this.translate.instant(`represented by old account of`)} ` + + this.getUserNameForExport(this.userRepo.getViewModel(date.delegation_user_merged_into_id)); + } const tableLine = [ { text: index }, { - text: - this.getUserNameForExport(date.user) + - (date.vote_delegated_to - ? `\n${this.translate.instant(`represented by`)} ` + - this.getUserNameForExport(date.vote_delegated_to) - : ``) + text: name + represented }, { text: this.translate.instant(date.voted ? `Yes` : `No`) diff --git a/client/src/app/site/pages/meetings/modules/poll/components/entitled-users-table/entitled-users-table.component.html b/client/src/app/site/pages/meetings/modules/poll/components/entitled-users-table/entitled-users-table.component.html index 6fd5d1f1ef..be1e42a874 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/entitled-users-table/entitled-users-table.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/entitled-users-table/entitled-users-table.component.html @@ -18,14 +18,35 @@ -
+
- ({{ 'represented by' | translate }} {{ entry.vote_delegated_to.getShortName().trim() }}) + ({{ 'represented by' | translate }} {{ entry.vote_delegated_to.getShortName() }}) + +
+
+ + {{ entry.delegation_user_merged_into }} + +
+
+ +
+ {{ entry.user_merged_into }} +
+ +
+ + ({{ 'represented by' | translate }} {{ entry.vote_delegated_to?.getShortName() }}) + +
+
+ + {{ entry.delegation_user_merged_into }}
- {{ 'Anonymous' | translate }} + {{ 'Anonymous' | translate }}
{{ 'Is present' | translate }}
diff --git a/client/src/app/site/pages/meetings/modules/poll/components/entitled-users-table/entitled-users-table.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/entitled-users-table/entitled-users-table.component.ts index eb4d01173b..3b2652615c 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/entitled-users-table/entitled-users-table.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/entitled-users-table/entitled-users-table.component.ts @@ -44,7 +44,14 @@ export class EntitledUsersTableComponent { public readonly permission = Permission; - public filterPropsEntitledUsersTable = [`user.full_name`, `vote_delegated_to.full_name`, `voted_verbose`]; + public filterPropsEntitledUsersTable = [ + `user.full_name`, + `vote_delegated_to.full_name`, + `user_merged_into`, + `delegation_user_merged_into`, + `voted_verbose` + ]; + public constructor( private controller: ParticipantControllerService, public filter: EntitledUsersListFilterService diff --git a/client/src/app/site/pages/meetings/modules/poll/definitions/entitled-users-table-entry.ts b/client/src/app/site/pages/meetings/modules/poll/definitions/entitled-users-table-entry.ts index f4a894254c..e40b09d33d 100644 --- a/client/src/app/site/pages/meetings/modules/poll/definitions/entitled-users-table-entry.ts +++ b/client/src/app/site/pages/meetings/modules/poll/definitions/entitled-users-table-entry.ts @@ -6,4 +6,6 @@ export interface EntitledUsersTableEntry extends EntitledUsersEntry, Identifiabl user?: ViewUser; voted_verbose: string; vote_delegated_to?: ViewUser | null; + user_merged_into?: string | null; + delegation_user_merged_into?: string | null; } diff --git a/client/src/app/site/pages/organization/pages/accounts/pages/account-list/account-list.module.ts b/client/src/app/site/pages/organization/pages/accounts/pages/account-list/account-list.module.ts index f2f480f38f..517c2dfd18 100644 --- a/client/src/app/site/pages/organization/pages/accounts/pages/account-list/account-list.module.ts +++ b/client/src/app/site/pages/organization/pages/accounts/pages/account-list/account-list.module.ts @@ -1,10 +1,13 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatDialogModule } from '@angular/material/dialog'; import { MatDividerModule } from '@angular/material/divider'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu'; +import { MatRadioModule } from '@angular/material/radio'; +import { MatTableModule } from '@angular/material/table'; import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; import { OpenSlidesTranslationModule } from 'src/app/site/modules/translations'; @@ -22,10 +25,11 @@ import { AccountCommonServiceModule } from '../../services/common/account-common import { AccountListRoutingModule } from './account-list-routing.module'; import { AccountListComponent } from './components/account-list/account-list.component'; import { AccountListMainComponent } from './components/account-list-main/account-list-main.component'; +import { AccountMergeDialogComponent } from './components/account-merge-dialog/account-merge-dialog.component'; import { AccountListServiceModule } from './services/account-list-service.module'; @NgModule({ - declarations: [AccountListComponent, AccountListMainComponent], + declarations: [AccountListComponent, AccountListMainComponent, AccountMergeDialogComponent], imports: [ CommonModule, AccountListRoutingModule, @@ -47,7 +51,10 @@ import { AccountListServiceModule } from './services/account-list-service.module MatFormFieldModule, IconContainerModule, RouterModule, - PipesModule + PipesModule, + MatDialogModule, + MatRadioModule, + MatTableModule ] }) export class AccountListModule {} diff --git a/client/src/app/site/pages/organization/pages/accounts/pages/account-list/components/account-list/account-list.component.html b/client/src/app/site/pages/organization/pages/accounts/pages/account-list/components/account-list/account-list.component.html index 896896eb37..cbc4eae45c 100644 --- a/client/src/app/site/pages/organization/pages/accounts/pages/account-list/components/account-list/account-list.component.html +++ b/client/src/app/site/pages/organization/pages/accounts/pages/account-list/components/account-list/account-list.component.html @@ -177,6 +177,10 @@

block {{ 'Enable/disable accounts' | translate }} ... +

diff --git a/client/src/app/site/pages/organization/pages/accounts/pages/account-list/components/account-list/account-list.component.ts b/client/src/app/site/pages/organization/pages/accounts/pages/account-list/components/account-list/account-list.component.ts index 613f80b9b0..038e151532 100644 --- a/client/src/app/site/pages/organization/pages/accounts/pages/account-list/components/account-list/account-list.component.ts +++ b/client/src/app/site/pages/organization/pages/accounts/pages/account-list/components/account-list/account-list.component.ts @@ -1,10 +1,13 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute } from '@angular/router'; import { marker as _ } from '@colsen1991/ngx-translate-extract-marker'; import { TranslateService } from '@ngx-translate/core'; +import { firstValueFrom } from 'rxjs'; import { Observable } from 'rxjs'; import { getOmlVerboseName } from 'src/app/domain/definitions/organization-permission'; import { OMLMapping } from 'src/app/domain/definitions/organization-permission'; +import { mediumDialogSettings } from 'src/app/infrastructure/utils/dialog-settings'; import { BaseListViewComponent } from 'src/app/site/base/base-list-view.component'; import { MeetingControllerService } from 'src/app/site/pages/meetings/services/meeting-controller.service'; import { ViewMeeting } from 'src/app/site/pages/meetings/view-models/view-meeting'; @@ -19,6 +22,7 @@ import { AccountControllerService } from '../../../../services/common/account-co import { AccountFilterService } from '../../../../services/common/account-filter.service'; import { AccountListSearchService } from '../../services/account-list-search/account-list-search.service'; import { AccountSortService } from '../../services/account-list-sort.service/account-sort.service'; +import { AccountMergeDialogComponent } from '../account-merge-dialog/account-merge-dialog.component'; const ACCOUNT_LIST_STORAGE_INDEX = `account_list`; @@ -47,7 +51,8 @@ export class AccountListComponent extends BaseListViewComponent { private userController: UserControllerService, public searchService: AccountListSearchService, private operator: OperatorService, - private vp: ViewPortService + private vp: ViewPortService, + private dialog: MatDialog ) { super(); super.setTitle(`Accounts`); @@ -126,4 +131,22 @@ export class AccountListComponent extends BaseListViewComponent { public getOmlByUser(user: ViewUser): string { return getOmlVerboseName(user.organization_management_level as keyof OMLMapping); } + + public async mergeUsersTogether(): Promise { + const result = await this.openMergeDialog(); + if (result) { + const id = result; + const user_ids = this.selectedRows.map(view => view.id).filter(sRid => sRid !== id); + this.controller.mergeTogether([{ id: id, user_ids: user_ids }]).resolve(); + } + } + + public async openMergeDialog(): Promise { + const data = { choices: this.selectedRows }; + const dialogRef = this.dialog.open(AccountMergeDialogComponent, { + ...mediumDialogSettings, + data: data + }); + return firstValueFrom(dialogRef.afterClosed()); + } } diff --git a/client/src/app/site/pages/organization/pages/accounts/pages/account-list/components/account-merge-dialog/account-merge-dialog.component.html b/client/src/app/site/pages/organization/pages/accounts/pages/account-list/components/account-merge-dialog/account-merge-dialog.component.html new file mode 100644 index 0000000000..394905c47e --- /dev/null +++ b/client/src/app/site/pages/organization/pages/accounts/pages/account-list/components/account-merge-dialog/account-merge-dialog.component.html @@ -0,0 +1,104 @@ +
{{ 'Merge users' | translate }}
+ +
{{ 'Please select a primary account.' | translate }}
+ + + + + + + + + + + + + + +
+
+
+
+
+
+ {{ user.short_name }} + + · {{ user.gender }} + + + · {{ user.pronoun }} + +
+
+ {{ user.short_name }} + + · {{ user.gender }} + + + · {{ user.pronoun }} + +
+
+   + + + +
+
+
+ {{ user.saml_id || user.username }} + · {{ user.email }}  + · {{ user.member_number }} +
+
+ {{ 'Last login' | translate }} {{ user.last_login | localizedDate }} +
+
+
+
+ + {{ user.meetings.length }} + + +
+
+
+
{{ 'Attention: Not selected accounts will be merged and then deleted.' | translate }}
+
+ {{ 'Warning: Data loss is possible, because two users are in the same meeting.' | translate }} + {{ 'Meetings effected:' | translate }} + {{ countMeetingsCollide }} +
+
+ + + + diff --git a/client/src/app/site/pages/organization/pages/accounts/pages/account-list/components/account-merge-dialog/account-merge-dialog.component.scss b/client/src/app/site/pages/organization/pages/accounts/pages/account-list/components/account-merge-dialog/account-merge-dialog.component.scss new file mode 100644 index 0000000000..c7ce3aa5ce --- /dev/null +++ b/client/src/app/site/pages/organization/pages/accounts/pages/account-list/components/account-merge-dialog/account-merge-dialog.component.scss @@ -0,0 +1,27 @@ +.error-color { + color: red; +} + +.mat-column-button { + width: 60px; +} + +.nameCell { + display: flex; + flex-direction: column; + flex-basis: 620px; +} + +.iconCell { + display: flex; + flex-direction: column; + flex-basis: 100px; +} + +.meta-info { + font-size: 12px; +} + +.crossed-out { + text-decoration: line-through; +} diff --git a/client/src/app/site/pages/organization/pages/accounts/pages/account-list/components/account-merge-dialog/account-merge-dialog.component.spec.ts b/client/src/app/site/pages/organization/pages/accounts/pages/account-list/components/account-merge-dialog/account-merge-dialog.component.spec.ts new file mode 100644 index 0000000000..83abcb8dc6 --- /dev/null +++ b/client/src/app/site/pages/organization/pages/accounts/pages/account-list/components/account-merge-dialog/account-merge-dialog.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AccountMergeDialogComponent } from './account-merge-dialog.component'; + +xdescribe(`AccountMergeDialogComponent`, () => { + let component: AccountMergeDialogComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [AccountMergeDialogComponent] + }); + fixture = TestBed.createComponent(AccountMergeDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it(`should create`, () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/pages/organization/pages/accounts/pages/account-list/components/account-merge-dialog/account-merge-dialog.component.ts b/client/src/app/site/pages/organization/pages/accounts/pages/account-list/components/account-merge-dialog/account-merge-dialog.component.ts new file mode 100644 index 0000000000..1f8bf407e9 --- /dev/null +++ b/client/src/app/site/pages/organization/pages/accounts/pages/account-list/components/account-merge-dialog/account-merge-dialog.component.ts @@ -0,0 +1,76 @@ +import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { ViewUser } from 'src/app/site/pages/meetings/view-models/view-user'; + +export type AccountMergeDialogData = { choices: ViewUser[] }; +export type AccountMergeDialogAnswer = number | null; + +@Component({ + selector: `os-account-merge-dialog`, + templateUrl: `./account-merge-dialog.component.html`, + styleUrls: [`./account-merge-dialog.component.scss`], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AccountMergeDialogComponent { + public constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: AccountMergeDialogData + ) { + this.data.choices.sort((a, b) => a.name.localeCompare(b.name)); + const meetingIdsVisited: number[] = []; + + for (const user of this.data.choices) { + for (const meeting of user.meetings) { + if (meetingIdsVisited.includes(meeting.id)) { + this._meetingsCollide.add(meeting.id); + } + meetingIdsVisited.push(meeting.id); + } + } + } + + public selectedUserId: number; + public displayedColumns = [`button`, `name`, `icon`]; + private _meetingsCollide = new Set(); + + public get showMeetingsCollide(): boolean { + return this._meetingsCollide.size > 0; + } + + public get countMeetingsCollide(): number { + return this._meetingsCollide.size; + } + + public get possibleChoices(): ViewUser[] { + return this.data.choices; + } + + public userMeetings(user: ViewUser): string { + if (user.meetings.length > 10) { + const res = user.meetings.map(a => a.name).slice(0, 10); + res.push(`...`); + return res.join(`\n`); + } else { + return user.meetings.map(a => a.name).join(`\n`); + } + } + + public isCrossedOut(user: ViewUser): boolean { + if (!this.selectedUserId) { + return false; + } + return this.selectedUserId !== user.id; + } + + public onChange(event): void { + this.selectedUserId = Number.parseInt(event.value); + } + + protected closeDialog(ok: boolean): void { + if (ok && this.selectedUserId) { + this.dialogRef.close(this.selectedUserId); + } else { + this.dialogRef.close(null); + } + } +} diff --git a/client/src/app/site/pages/organization/pages/accounts/services/common/account-controller.service.ts b/client/src/app/site/pages/organization/pages/accounts/services/common/account-controller.service.ts index 42d6228b9d..a388837c3c 100644 --- a/client/src/app/site/pages/organization/pages/accounts/services/common/account-controller.service.ts +++ b/client/src/app/site/pages/organization/pages/accounts/services/common/account-controller.service.ts @@ -96,4 +96,8 @@ export class AccountControllerService extends BaseController { public import(payload: { id: number; import: boolean }[]): Action { return this.repo.accountImport(payload); } + + public mergeTogether(payload: { id: number; user_ids: number[] }[]): Action { + return this.repo.mergeTogether(payload); + } } diff --git a/client/src/app/ui/modules/icon-container/components/icon-container/icon-container.component.html b/client/src/app/ui/modules/icon-container/components/icon-container/icon-container.component.html index 827804fa26..d7bd884818 100644 --- a/client/src/app/ui/modules/icon-container/components/icon-container/icon-container.component.html +++ b/client/src/app/ui/modules/icon-container/components/icon-container/icon-container.component.html @@ -17,6 +17,7 @@ [inline]="inline" [matTooltip]="iconTooltip" [matTooltipPosition]="iconTooltipPosition" + [matTooltipClass]="iconTooltipClass" [ngClass]="{ pointer: iconAction, mirrored: mirrored }" [ngStyle]="{ transform: getRotation() }" (click)="iconClick()" diff --git a/client/src/app/ui/modules/icon-container/components/icon-container/icon-container.component.ts b/client/src/app/ui/modules/icon-container/components/icon-container/icon-container.component.ts index 4ac6185de4..04d0ace021 100644 --- a/client/src/app/ui/modules/icon-container/components/icon-container/icon-container.component.ts +++ b/client/src/app/ui/modules/icon-container/components/icon-container/icon-container.component.ts @@ -73,6 +73,12 @@ export class IconContainerComponent { @Input() public iconTooltipPosition: TooltipPosition = `below`; + /** + * Optional string for tooltip class + */ + @Input() + public iconTooltipClass = ``; + /** * Uses a css class for nowrap */