From 0a160f213561f1c91b21290aaa03c766e6e50dbb Mon Sep 17 00:00:00 2001 From: luisa-beerboom <101706784+luisa-beerboom@users.noreply.github.com> Date: Tue, 25 Jun 2024 17:03:24 +0200 Subject: [PATCH] Redesign election ballots (#2214) --- .../users/user-repository.service.ts | 9 +- .../poll/base/base-poll-vote.component.ts | 188 ------- .../base-poll-vote.component.html | 260 +++++++++ .../base-poll-vote.component.scss | 209 ++++++++ .../base-poll-vote.component.ts | 499 ++++++++++++++++++ .../poll-cannot-vote-message.component.html | 33 ++ .../poll-cannot-vote-message.component.scss | 23 + ...poll-cannot-vote-message.component.spec.ts | 22 + .../poll-cannot-vote-message.component.ts | 57 ++ .../meetings/modules/poll/poll.module.ts | 6 +- .../topic-poll-vote.component.html | 175 ------ .../topic-poll-vote.component.scss | 91 ---- .../topic-poll-vote.component.ts | 280 +--------- .../modules/topic-poll/topic-poll.module.ts | 2 + .../assignment-poll/assignment-poll.module.ts | 2 + .../assignment-poll-vote.component.html | 239 --------- .../assignment-poll-vote.component.scss | 91 ---- .../assignment-poll-vote.component.ts | 272 +--------- .../motion-poll-vote.component.html | 90 ---- .../motion-poll-vote.component.scss | 56 -- .../motion-poll-vote.component.ts | 59 +-- .../modules/motion-poll/motion-poll.module.ts | 2 + .../custom-icon/custom-icon.component.html | 3 + .../custom-icon/custom-icon.component.scss | 0 .../custom-icon/custom-icon.component.spec.ts | 21 + .../custom-icon/custom-icon.component.ts | 24 + .../modules/custom-icon/custom-icon.module.ts | 11 + .../modules/custom-icon/definitions/index.ts | 7 + .../src/app/ui/modules/custom-icon/index.ts | 1 + client/src/assets/img/drawn_cross.svg | 64 +++ .../styles/global-components-style.scss | 10 + .../src/assets/styles/poll-styles-common.scss | 12 +- client/src/meta | 2 +- 33 files changed, 1325 insertions(+), 1495 deletions(-) delete mode 100644 client/src/app/site/pages/meetings/modules/poll/base/base-poll-vote.component.ts create mode 100644 client/src/app/site/pages/meetings/modules/poll/components/base-poll-vote/base-poll-vote.component.html create mode 100644 client/src/app/site/pages/meetings/modules/poll/components/base-poll-vote/base-poll-vote.component.scss create mode 100644 client/src/app/site/pages/meetings/modules/poll/components/base-poll-vote/base-poll-vote.component.ts create mode 100644 client/src/app/site/pages/meetings/modules/poll/components/poll-cannot-vote-message/poll-cannot-vote-message.component.html create mode 100644 client/src/app/site/pages/meetings/modules/poll/components/poll-cannot-vote-message/poll-cannot-vote-message.component.scss create mode 100644 client/src/app/site/pages/meetings/modules/poll/components/poll-cannot-vote-message/poll-cannot-vote-message.component.spec.ts create mode 100644 client/src/app/site/pages/meetings/modules/poll/components/poll-cannot-vote-message/poll-cannot-vote-message.component.ts delete mode 100644 client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-vote/topic-poll-vote.component.html delete mode 100644 client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-vote/topic-poll-vote.component.scss delete mode 100644 client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-vote/assignment-poll-vote.component.html delete mode 100644 client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-vote/assignment-poll-vote.component.scss delete mode 100644 client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-vote/motion-poll-vote.component.html delete mode 100644 client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-vote/motion-poll-vote.component.scss create mode 100644 client/src/app/ui/modules/custom-icon/components/custom-icon/custom-icon.component.html create mode 100644 client/src/app/ui/modules/custom-icon/components/custom-icon/custom-icon.component.scss create mode 100644 client/src/app/ui/modules/custom-icon/components/custom-icon/custom-icon.component.spec.ts create mode 100644 client/src/app/ui/modules/custom-icon/components/custom-icon/custom-icon.component.ts create mode 100644 client/src/app/ui/modules/custom-icon/custom-icon.module.ts create mode 100644 client/src/app/ui/modules/custom-icon/definitions/index.ts create mode 100644 client/src/app/ui/modules/custom-icon/index.ts create mode 100644 client/src/assets/img/drawn_cross.svg 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 c7ad164694..413bce9313 100644 --- a/client/src/app/gateways/repositories/users/user-repository.service.ts +++ b/client/src/app/gateways/repositories/users/user-repository.service.ts @@ -329,11 +329,14 @@ export class UserRepositoryService extends BaseRepository { } private getLevelAndNumber(user: LevelAndNumberInformation): string { + const strings: string[] = []; + if (user.structureLevels()) { + strings.push(user.structureLevels()); + } if (user.number()) { - return `${this.translate.instant(`No.`)} ${user.number()}`; - } else { - return ``; + strings.push(`${this.translate.instant(`No.`)} ${user.number()}`); } + return strings.join(` ยท `); } public getVerboseName = (plural = false): string => this.translate.instant(plural ? `Participants` : `Participant`); diff --git a/client/src/app/site/pages/meetings/modules/poll/base/base-poll-vote.component.ts b/client/src/app/site/pages/meetings/modules/poll/base/base-poll-vote.component.ts deleted file mode 100644 index a0ce350b3a..0000000000 --- a/client/src/app/site/pages/meetings/modules/poll/base/base-poll-vote.component.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { ChangeDetectorRef, Directive, inject, Input } from '@angular/core'; -import { BehaviorSubject, debounceTime, Observable } from 'rxjs'; -import { Id } from 'src/app/domain/definitions/key-types'; -import { - IdentifiedVotingData, - PollContentObject, - PollPropertyVerbose, - VoteValue, - VotingData -} from 'src/app/domain/models/poll'; -import { BaseComponent } from 'src/app/site/base/base.component'; -import { PollControllerService } from 'src/app/site/pages/meetings/modules/poll/services/poll-controller.service'; -import { ViewPoll } from 'src/app/site/pages/meetings/pages/polls'; -import { ViewUser } from 'src/app/site/pages/meetings/view-models/view-user'; -import { OperatorService } from 'src/app/site/services/operator.service'; - -import { MeetingSettingsService } from '../../../services/meeting-settings.service'; -import { VoteControllerService } from '../services/vote-controller.service'; -import { VotingProhibition, VotingService } from '../services/voting.service'; - -export interface VoteOption { - vote?: VoteValue; - css?: string; - icon?: string; - label: string; -} - -@Directive() -export abstract class BasePollVoteComponent extends BaseComponent { - @Input() - public set poll(value: ViewPoll) { - this._poll = value; - this.updatePoll(); - } - - public get poll(): ViewPoll { - return this._poll; - } - - public votingErrors = VotingProhibition; - - public get isReady(): boolean { - return this._isReady; - } - - public get isUserPresent(): boolean { - return this.user?.isPresentInMeeting(); - } - - public PollPropertyVerbose = PollPropertyVerbose; - - public delegations: ViewUser[] = []; - - protected voteRequestData: IdentifiedVotingData = {}; - - protected alreadyVoted: { [userId: number]: boolean } = {}; - - protected deliveringVote: { [userId: number]: boolean } = {}; - - protected user!: ViewUser; - - public voteDelegationEnabled: Observable = - this.meetingSettingsService.get(`users_enable_vote_delegations`); - - public forbidDelegationToVote: Observable = - this.meetingSettingsService.get(`users_forbid_delegator_to_vote`); - - private _isReady = false; - private _poll!: ViewPoll; - private _delegationsMap: { [userId: number]: ViewUser } = {}; - private _canVoteForSubjectMap: { [userId: number]: BehaviorSubject } = {}; - - private voteRepo = inject(VoteControllerService); - protected votingService = inject(VotingService); - protected cd = inject(ChangeDetectorRef); - private pollRepo = inject(PollControllerService); - private operator = inject(OperatorService); - - public constructor(private meetingSettingsService: MeetingSettingsService) { - super(); - this.subscriptions.push( - this.operator.userObservable.pipe(debounceTime(50)).subscribe(user => { - if ( - user && - (!user.getMeetingUser()?.vote_delegated_to_id || user.getMeetingUser()?.vote_delegated_to) - ) { - this.user = user; - this.delegations = user.vote_delegations_from(); - this.createVotingDataObjects(); - - for (const key of Object.keys(this._canVoteForSubjectMap)) { - this._canVoteForSubjectMap[+key].next(this.canVote(this._delegationsMap[+key])); - } - - this.cd.markForCheck(); - this._isReady = true; - } - }) - ); - } - - public isDeliveringVote(user: ViewUser = this.user): boolean { - return this.deliveringVote[user?.id]; - } - - public hasAlreadyVoted(user: ViewUser = this.user): boolean { - return this.alreadyVoted[user?.id]; - } - - public canVoteForObservable(user: ViewUser = this.user): Observable { - if (!this._canVoteForSubjectMap[user.id]) { - this._canVoteForSubjectMap[user.id] = new BehaviorSubject(this.canVote(user)); - } - return this._canVoteForSubjectMap[user.id]; - } - - public getVotingError(user: ViewUser = this.user): string { - return this.votingService.getVotingProhibitionReasonVerbose(this.poll, user) || ``; - } - - public getVotingErrorFromName(errorName: string): string { - return this.votingService.getVotingProhibitionReasonVerboseFromName(errorName) || ``; - } - - protected async sendVote(userId: Id, votePayload: any): Promise { - try { - await this.pollRepo.vote(this.poll, votePayload); - this.alreadyVoted[userId] = true; - this.poll.hasVoted = true; // Set it manually to `true`, because the server will do the same - } catch (e: any) { - this.raiseError(e); - } finally { - this.deliveringVote[userId] = false; - this.cd.markForCheck(); - } - } - - private createVotingDataObjects(): void { - this.voteRequestData[this.user.id] = { value: {} } as VotingData; - this.alreadyVoted[this.user.id] = this.poll.hasVoted; - this.deliveringVote[this.user.id] = false; - - if (this.delegations) { - this.setupDelegations(); - } - } - - protected updatePoll(): void { - this.setupHasVotedSubscription(); - } - - private setupHasVotedSubscription(): void { - this.subscriptions.push( - this.voteRepo.subscribeVoted(this.poll).subscribe(() => { - if (this.user) { - this.alreadyVoted[this.user.id] = this.poll.hasVoted; - if (this.delegations) { - this.setupDelegations(); - } - } - - for (const key of Object.keys(this._canVoteForSubjectMap)) { - this._canVoteForSubjectMap[+key].next(this.canVote(this._delegationsMap[+key])); - } - }) - ); - } - - private setupDelegations(): void { - for (const delegation of this.delegations) { - this._delegationsMap[delegation.id] = delegation; - this.alreadyVoted[delegation.id] = this.poll.hasVotedForDelegations(delegation.id); - if (!this.voteRequestData[delegation.id]) { - this.voteRequestData[delegation.id] = { value: {} } as VotingData; - this.deliveringVote[delegation.id] = false; - } - } - } - - private canVote(user: ViewUser = this.user): boolean { - return ( - this.votingService.canVote(this.poll, user) && - !this.isDeliveringVote(user) && - !this.hasAlreadyVoted(user) && - this.hasAlreadyVoted(user) !== undefined - ); - } -} diff --git a/client/src/app/site/pages/meetings/modules/poll/components/base-poll-vote/base-poll-vote.component.html b/client/src/app/site/pages/meetings/modules/poll/components/base-poll-vote/base-poll-vote.component.html new file mode 100644 index 0000000000..02554c600f --- /dev/null +++ b/client/src/app/site/pages/meetings/modules/poll/components/base-poll-vote/base-poll-vote.component.html @@ -0,0 +1,260 @@ + + + + + + +
+ + + +
+
+
+ + +

+ {{ 'Voting right for' | translate }} +  {{ delegation.getFullName() }} +

+ + + + +

+ {{ pollHint }} +

+
+
+ +
+ {{ poll.option_ids.length }} {{ optionPluralLabel | translate }} +
+ + {{ 'Available votes' | translate }}: + {{ getVotesAvailable(delegation) }}/{{ poll.max_votes_amount }} + + + ({{ 'At least' | translate }} + {{ poll.min_votes_amount }} + ) + + + + ({{ 'At most' | translate }} + {{ poll.max_votes_per_option }} + {{ maxVotesPerOptionSuffix | translate }}) + + +
+ + +
+
+ + {{ poll.option_ids.length }} {{ optionPluralLabel | translate }} + + + {{ action.label | translate }} + +
+ +
+
+ + {{ option.content_object.short_name }} +
+ {{ option.content_object.getLevelAndNumber() }} +
+
+ + {{ option.text }} + + {{ noDataLabel | translate }} +
+
+ + {{ option.content_object.getTitle() | translate }} + +
+ + +
+ + + {{ action.label | translate }} + +
+
+ + + + {{ getErrorInVoteEntry(option.id) }} + + +
+
+ +
+
+
+
+
OR
+
+
+
+
+ + + +
+
+ + {{ getGlobalOptionName(option) | translate }} + +
+
+ +
+
+
+
+
+
+ + + +
+ + +
+
+ + +
+ +
+
diff --git a/client/src/app/site/pages/meetings/modules/poll/components/base-poll-vote/base-poll-vote.component.scss b/client/src/app/site/pages/meetings/modules/poll/components/base-poll-vote/base-poll-vote.component.scss new file mode 100644 index 0000000000..b5e55820d4 --- /dev/null +++ b/client/src/app/site/pages/meetings/modules/poll/components/base-poll-vote/base-poll-vote.component.scss @@ -0,0 +1,209 @@ +@import 'src/assets/styles/poll-colors.scss'; +@import 'src/assets/styles/poll-styles-common.scss'; + +%vote-grid-base { + display: grid; + grid-gap: 10px; + padding: 10px; + margin: 0 10px; + + .vote-button { + min-width: 50px; + height: 50px; + margin: auto; + + .vote-button-content { + margin: auto; + display: grid; + + .vote-button-circle, + .vote-button-cross { + grid-column: 1; + grid-row: 1; + } + + .vote-button-circle { + z-index: 0; + } + + .vote-button-cross { + z-index: 1; + height: 24px; + width: 24px; + opacity: 0; + } + } + + .button-content-opaque { + opacity: 0.5; + } + + .button-content-not-opaque { + opacity: 1 !important; + } + } + + .vote-button-area { + display: inline-grid; + justify-content: center; + } + + .vote-button:hover { + .button-content-opaque { + opacity: 1; + } + } + + .vote-label { + margin-left: 10px; + } + + .poll-option-title { + text-align: center; + } +} + +.or-divider-grid { + @extend %vote-grid-base; + grid-template-areas: 'left or right'; + grid-template-columns: auto min-content auto; + + .hr-wrapper { + height: 100%; + } +} + +.poll-vote-delegation { + margin-top: 1em; + + .poll-delegation-title { + font-weight: 500; + } +} + +.yn-grid { + @extend %vote-grid-base; + grid-template-areas: 'name yes no'; + grid-template-columns: auto var(--poll-option-title-width, 70px) var(--poll-option-title-width, 70px); + @media (max-width: 700px) { + grid-template-areas: 'name name name' '. yes no'; + } +} + +.yna-grid { + @extend %vote-grid-base; + grid-template-areas: 'name yes no abstain'; + grid-template-columns: auto var(--poll-option-title-width, 70px) var(--poll-option-title-width, 70px) var( + --poll-option-title-width, + 70px + ); + @media (max-width: 700px) { + grid-template-areas: 'name name name name' '. yes no abstain'; + } +} + +.single-vote-grid { + @extend %vote-grid-base; + grid-template-areas: 'name yes'; + grid-template-columns: auto var(--poll-option-title-width, 70px); + @media (max-width: 700px) { + grid-template-areas: 'name name' '. yes'; + } +} + +.single-multi-vote-grid { + @extend %vote-grid-base; + grid-template-areas: 'name yes'; + grid-template-columns: auto 120px; + @media (max-width: 700px) { + grid-template-areas: 'name name' '. yes'; + } +} + +.global-option-grid { + @extend %vote-grid-base; + grid-template-columns: auto var(--poll-option-title-width, 70px); +} + +.option-list-information-grid { + @extend %vote-grid-base; + grid-template-columns: max-content auto max-content; +} + +.split-grid { + display: grid; + grid-gap: 20px; + margin-top: 2em; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); + + .vote-button-area { + display: inline-grid; + grid-gap: 1em; + margin: auto; + + .vote-label { + text-align: center; + } + } +} + +.grid-name-area { + grid-area: name; +} + +.vote-option-title { + display: flex; + span, + i { + margin-top: auto; + margin-bottom: auto; + } +} + +.option0 { + grid-area: yes; +} + +.option1 { + grid-area: no; +} + +.option2 { + grid-area: abstain; +} + +.strike-text { + text-decoration: line-through; +} + +.centered-button-wrapper { + display: flex; + text-align: center; + margin-top: 20px; + > * { + margin-left: auto; + margin-right: auto; + } + + .vote-submitted { + color: $votes-yes-color; + font-size: 200%; + overflow: visible; + } +} + +.vote-input { + width: 120px; +} + +.mat-divider-horizontal { + position: initial; +} + +.submit-vote-indicator { + text-align: center; + .mat-mdc-progress-spinner { + display: block; + margin: auto; + } +} diff --git a/client/src/app/site/pages/meetings/modules/poll/components/base-poll-vote/base-poll-vote.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/base-poll-vote/base-poll-vote.component.ts new file mode 100644 index 0000000000..0b1a014f11 --- /dev/null +++ b/client/src/app/site/pages/meetings/modules/poll/components/base-poll-vote/base-poll-vote.component.ts @@ -0,0 +1,499 @@ +import { ChangeDetectorRef, Directive, inject, Input, OnInit } from '@angular/core'; +import { UntypedFormControl, Validators } from '@angular/forms'; +import { marker as _ } from '@colsen1991/ngx-translate-extract-marker'; +import { BehaviorSubject, debounceTime, Observable } from 'rxjs'; +import { Id } from 'src/app/domain/definitions/key-types'; +import { + GlobalVote, + IdentifiedVotingData, + PollContentObject, + PollPropertyVerbose, + PollType, + VoteValue, + VotingData +} from 'src/app/domain/models/poll'; +import { BaseComponent } from 'src/app/site/base/base.component'; +import { PollControllerService } from 'src/app/site/pages/meetings/modules/poll/services/poll-controller.service'; +import { ViewOption, ViewPoll } from 'src/app/site/pages/meetings/pages/polls'; +import { ViewUser } from 'src/app/site/pages/meetings/view-models/view-user'; +import { OperatorService } from 'src/app/site/services/operator.service'; +import { CustomIcon } from 'src/app/ui/modules/custom-icon/definitions'; + +import { MeetingSettingsService } from '../../../../services/meeting-settings.service'; +import { VoteControllerService } from '../../services/vote-controller.service'; +import { VotingProhibition, VotingService } from '../../services/voting.service'; + +export interface VoteOption { + vote?: VoteValue; + css?: string; + label: string; +} + +export interface PollVoteViewSettings { + hideLeftoverVotes?: boolean; + hideGlobalOptions?: boolean; + hideSendNow?: boolean; + isSplitSingleOption?: boolean; +} + +@Directive() +export abstract class BasePollVoteComponent extends BaseComponent implements OnInit { + public readonly drawnCross = CustomIcon.DRAWN_CROSS; + + @Input() + public set poll(value: ViewPoll) { + this._poll = value; + this.updatePoll(); + } + + public get poll(): ViewPoll { + return this._poll; + } + + public PollType = PollType; + public formControlMap: { [optionId: number]: UntypedFormControl } = {}; + + public get minVotes(): number { + return this.poll.min_votes_amount; + } + + public votingErrors = VotingProhibition; + + public get isReady(): boolean { + return this._isReady; + } + + public get isUserPresent(): boolean { + return this.user?.isPresentInMeeting(); + } + + public PollPropertyVerbose = PollPropertyVerbose; + + public delegations: ViewUser[] = []; + + public readonly voteOptions: VoteOption[] = [ + { + vote: `Y`, + css: `voted-yes`, + label: `Yes` + }, + { + vote: `N`, + css: `voted-no`, + label: `No` + }, + { + vote: `A`, + css: `voted-abstain`, + label: `Abstain` + } + ]; + + /** + * Subset of voteOptions that is used based on the pollmethod. + */ + public voteActions: VoteOption[] = []; + + public get showAvailableVotes(): boolean { + return (this.poll.isMethodY || this.poll.isMethodN) && this.poll.max_votes_amount > 1; + } + + /** + * Subset of global voteOptions that is used. + */ + public globalVoteActions: VoteOption[] = []; + + public get pollHint(): string { + return null; + } + + public readonly settings: PollVoteViewSettings = {}; + + public readonly noDataLabel: string = _(`No data`); + + public readonly maxVotesPerOptionSuffix: string = _(`votes per option`); + + public readonly optionPluralLabel: string = _(`Options`); + + public voteDelegationEnabled: Observable = + this.meetingSettingsService.get(`users_enable_vote_delegations`); + + public forbidDelegationToVote: Observable = + this.meetingSettingsService.get(`users_forbid_delegator_to_vote`); + + protected voteRequestData: IdentifiedVotingData = {}; + + protected alreadyVoted: { [userId: number]: boolean } = {}; + + protected deliveringVote: { [userId: number]: boolean } = {}; + + protected user!: ViewUser; + + private _isReady = false; + private _poll!: ViewPoll; + private _delegationsMap: { [userId: number]: ViewUser } = {}; + private _canVoteForSubjectMap: { [userId: number]: BehaviorSubject } = {}; + + private voteRepo = inject(VoteControllerService); + + protected votingService = inject(VotingService); + protected cd = inject(ChangeDetectorRef); + private pollRepo = inject(PollControllerService); + private operator = inject(OperatorService); + + public constructor(private meetingSettingsService: MeetingSettingsService) { + super(); + this.updatePollOptionTitleWidth(); + this.subscriptions.push( + this.operator.userObservable.pipe(debounceTime(50)).subscribe(user => { + if ( + user && + (!user.getMeetingUser()?.vote_delegated_to_id || user.getMeetingUser()?.vote_delegated_to) + ) { + this.user = user; + this.delegations = user.vote_delegations_from(); + this.voteRequestData[this.user.id] = { value: {} } as VotingData; + this.alreadyVoted[this.user.id] = this.poll.hasVoted; + if (this.delegations) { + this.setupDelegations(); + } + + for (const key of Object.keys(this._canVoteForSubjectMap)) { + this._canVoteForSubjectMap[+key].next(this.canVote(this._delegationsMap[+key])); + } + + this.cd.markForCheck(); + this._isReady = true; + } + }), + this.translate.onLangChange.subscribe(() => { + this.updatePollOptionTitleWidth(); + }) + ); + } + + public ngOnInit(): void { + this.defineVoteOptions(); + this.cd.markForCheck(); + } + + private updatePollOptionTitleWidth(): void { + document.documentElement.style.setProperty( + `--poll-option-title-width`, + `${Math.max( + Math.max(...this.voteOptions.map(option => this.translate.instant(option.label).length * 9)), + 70 + )}px` + ); + } + + public isDeliveringVote(user: ViewUser = this.user): boolean { + return this.deliveringVote[user?.id]; + } + + public hasAlreadyVoted(user: ViewUser = this.user): boolean { + return this.alreadyVoted[user?.id]; + } + + public canVoteForObservable(user: ViewUser = this.user): Observable { + if (!this._canVoteForSubjectMap[user.id]) { + this._canVoteForSubjectMap[user.id] = new BehaviorSubject(this.canVote(user)); + } + return this._canVoteForSubjectMap[user.id]; + } + + public getVotingError(user: ViewUser = this.user): string { + return this.votingService.getVotingProhibitionReasonVerbose(this.poll, user) || ``; + } + + public getVotingErrorFromName(errorName: string): string { + return this.votingService.getVotingProhibitionReasonVerboseFromName(errorName) || ``; + } + + public getVotesCount(user: ViewUser = this.user): number { + if (this.voteRequestData[user?.id]) { + if (this.poll.isMethodY && this.poll.max_votes_per_option > 1 && !this.isGlobalOptionSelected(user)) { + return Object.keys(this.voteRequestData[user.id].value) + .map(key => parseInt(this.voteRequestData[user.id].value[+key] as string, 10)) + .reduce((a, b) => a + b, 0); + } else { + return Object.keys(this.voteRequestData[user.id].value).filter( + key => this.voteRequestData[user.id].value[+key] + ).length; + } + } + return 0; + } + + public getVotesAvailable(user: ViewUser = this.user): number | string { + if (this.isGlobalOptionSelected()) { + return `-`; + } + return this.poll.max_votes_amount - this.getVotesCount(user); + } + + public getFormControl(optionId: number): UntypedFormControl { + if (!this.formControlMap[optionId]) { + this.formControlMap[optionId] = new UntypedFormControl(0, [ + Validators.required, + Validators.min(0), + Validators.max(this.poll.max_votes_per_option) + ]); + } + return this.formControlMap[optionId]; + } + + public getErrorInVoteEntry(optionId: number): string { + if (this.formControlMap[optionId].hasError(`required`)) { + return this.translate.instant(`This is not a number.`); + } else if (this.formControlMap[optionId].hasError(`min`)) { + return this.translate.instant(`Negative votes are not allowed.`); + } else if (this.formControlMap[optionId].hasError(`max`)) { + return this.translate.instant(`Too many votes on one option.`); + } + return ``; + } + + public abstract getActionButtonClass(actions: VoteOption, option: ViewOption, user: ViewUser): string; + + public getActionButtonContentClass(voteOption: VoteOption, option: ViewOption, user: ViewUser = this.user): string { + return this.getActionButtonClass(voteOption, option, user) ? `` : `button-content-opaque`; + } + + public getGlobalButtonContentClass(option: VoteOption, user: ViewUser = this.user): string { + return this.getGlobalCSSClass(option, user) ? `` : `button-content-opaque`; + } + + protected isGlobalOptionSelected(user: ViewUser = this.user): boolean { + const value = this.voteRequestData[user.id]?.value; + return value === `Y` || value === `N` || value === `A`; + } + + public saveGlobalVote(globalVote: GlobalVote, user: ViewUser = this.user): void { + if (this.voteRequestData[user.id].value && this.voteRequestData[user.id].value === globalVote) { + this.voteRequestData[user.id].value = {}; + if (this.poll.isMethodY && this.poll.max_votes_per_option > 1) { + this.enableInputs(); + } + } else { + this.voteRequestData[user.id].value = globalVote; + if (this.poll.isMethodY && this.poll.max_votes_per_option > 1) { + this.disableAndResetInputs(); + } + this.submitVote(user); + } + } + + public async submitVote(user: ViewUser, value: any = undefined): Promise { + this.deliveringVote[user.id] = true; + this.cd.markForCheck(); + + const votePayload = { + value: value, + user_id: user.id + }; + + await this.sendVote(user.id, votePayload); + } + + public getGlobalCSSClass(option: VoteOption, user: ViewUser = this.user): string { + if (this.voteRequestData[user.id]?.value === option.vote) { + return `button-content-not-opaque`; + } + return ``; + } + + public getGlobalOptionName(option: VoteOption): string { + switch (option.label) { + case `Yes`: + return `General approval`; + case `No`: + return `General rejection`; + default: + return `General ` + option.label.toLowerCase(); + } + } + + public saveMultipleVotes(optionId: number, event: any, user: ViewUser = this.user): void { + let vote = parseInt(event.target.value, 10); + + if (isNaN(vote) || vote > this.poll.max_votes_per_option || vote < 0) { + vote = 0; + } + + if (!this.voteRequestData[user.id]) { + throw new Error(`The user for your voting request does not exist`); + } + + if (this.isGlobalOptionSelected(user)) { + delete this.voteRequestData[user.id].value; + } + + if (this.poll.isMethodY && this.poll.max_votes_per_option > 1) { + this.saveMultipleVotesMultiVoteMethodY(optionId, vote, user); + } + } + + public abstract saveSingleVote(optionId: number, vote: VoteValue, user: ViewUser): void; + public abstract shouldStrikeOptionText(option: ViewOption, user: ViewUser): boolean; + + protected async sendVote(userId: Id, votePayload: any): Promise { + try { + await this.pollRepo.vote(this.poll, votePayload); + this.alreadyVoted[userId] = true; + this.poll.hasVoted = true; // Set it manually to `true`, because the server will do the same + } catch (e: any) { + this.raiseError(e); + } finally { + this.deliveringVote[userId] = false; + this.cd.markForCheck(); + } + } + + protected isErrorInVoteEntry(): boolean { + for (const key in this.formControlMap) { + if (this.formControlMap.hasOwnProperty(key) && this.formControlMap[key].invalid) { + return true; + } + } + return false; + } + + private saveMultipleVotesMultiVoteMethodY(optionId: number, vote: number, user: ViewUser = this.user): void { + // Another option is not expected here + const maxVotesAmount = this.poll.max_votes_amount; + const tmpVoteRequest = this.getTmpVoteRequestMultipleVotes(optionId, vote, user); + + // check if you can still vote + const countedVotes = Object.keys(tmpVoteRequest) + .map(key => parseInt(tmpVoteRequest[key], 10)) + .reduce((a, b) => a + b, 0); + if (countedVotes <= maxVotesAmount) { + this.voteRequestData[user.id].value = tmpVoteRequest; + + // if you have no options anymore, try to send + if (this.getVotesCount(user) === maxVotesAmount && !this.isErrorInVoteEntry()) { + this.submitVote(user); + } + } else { + this.raiseError( + this.translate.instant(`You reached the maximum amount of votes. Deselect one option first.`) + ); + this.formControlMap[optionId].setValue(this.voteRequestData[user.id].value[optionId]); + } + } + + private getTmpVoteRequestMultipleVotes( + optionId: number, + vote: number, + user: ViewUser = this.user + ): { [option_id: number]: number } { + const maxVotesAmount = this.poll.max_votes_amount; + const maxVotesPerOption = this.poll.max_votes_per_option; + return this.poll.options + .map(option => option.id) + .reduce((output, next_id) => { + output[next_id] = this.voteRequestData[user.id].value[next_id]; + output[next_id] = output[next_id] ? output[next_id] : 0; + if (next_id === optionId) { + if (vote > Math.min(maxVotesPerOption, maxVotesAmount)) { + output[next_id] = Math.min(maxVotesPerOption, maxVotesAmount); + } else if (vote >= 0) { + output[next_id] = vote; + } + } + return output; + }, {}); + } + + private enableInputs(): void { + for (const key in this.formControlMap) { + if (this.formControlMap.hasOwnProperty(key)) { + this.formControlMap[key].enable(); + } + } + } + + private disableAndResetInputs(): void { + for (const key in this.formControlMap) { + if (this.formControlMap.hasOwnProperty(key)) { + this.formControlMap[key].setValue(0); + this.formControlMap[key].disable(); + } + } + } + + private createVotingDataObjects(): void { + this.voteRequestData[this.user.id] = { value: {} } as VotingData; + this.alreadyVoted[this.user.id] = this.poll.hasVoted; + this.deliveringVote[this.user.id] = false; + + if (this.delegations) { + this.setupDelegations(); + } + } + + protected updatePoll(): void { + this.setupHasVotedSubscription(); + this.defineVoteOptions(); + } + + private setupHasVotedSubscription(): void { + this.subscriptions.push( + this.voteRepo.subscribeVoted(this.poll).subscribe(() => { + if (this.user) { + this.alreadyVoted[this.user.id] = this.poll.hasVoted; + if (this.delegations) { + this.setupDelegations(); + } + } + + for (const key of Object.keys(this._canVoteForSubjectMap)) { + this._canVoteForSubjectMap[+key].next(this.canVote(this._delegationsMap[+key])); + } + }) + ); + } + + private setupDelegations(): void { + for (const delegation of this.delegations) { + this._delegationsMap[delegation.id] = delegation; + this.alreadyVoted[delegation.id] = this.poll.hasVotedForDelegations(delegation.id); + if (!this.voteRequestData[delegation.id]) { + this.voteRequestData[delegation.id] = { value: {} } as VotingData; + this.deliveringVote[delegation.id] = false; + } + } + } + + private canVote(user: ViewUser = this.user): boolean { + return ( + this.votingService.canVote(this.poll, user) && + !this.isDeliveringVote(user) && + !this.hasAlreadyVoted(user) && + this.hasAlreadyVoted(user) !== undefined + ); + } + + private defineVoteOptions(): void { + this.voteActions = []; + this.globalVoteActions = []; + if (this.poll) { + const globals = { + Y: this.poll.global_yes, + N: this.poll.global_no, + A: this.poll.global_abstain + }; + + for (const option of this.voteOptions) { + if (this.poll.pollmethod.includes(option.vote)) { + this.voteActions.push(option); + } + + if (globals[option.vote]) { + this.globalVoteActions.push(option); + } + } + } + } +} diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-cannot-vote-message/poll-cannot-vote-message.component.html b/client/src/app/site/pages/meetings/modules/poll/components/poll-cannot-vote-message/poll-cannot-vote-message.component.html new file mode 100644 index 0000000000..4918eaf32f --- /dev/null +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-cannot-vote-message/poll-cannot-vote-message.component.html @@ -0,0 +1,33 @@ +
+ +
+ +
+ {{ 'Voting successful.' | translate }} +
+ + +
+ +
+ {{ 'Delivering vote... Please wait!' | translate }} +
+ +
+ +
+ {{ 'Retrieving vote status... Please wait!' | translate }} +
+ + + +
+ {{ getVotingError(delegationUser) | translate }} +
+ + +
+ {{ getVotingErrorFromName('USER_NOT_PRESENT') | translate }} +
+
+
diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-cannot-vote-message/poll-cannot-vote-message.component.scss b/client/src/app/site/pages/meetings/modules/poll/components/poll-cannot-vote-message/poll-cannot-vote-message.component.scss new file mode 100644 index 0000000000..cf7543b8fb --- /dev/null +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-cannot-vote-message/poll-cannot-vote-message.component.scss @@ -0,0 +1,23 @@ +@import 'src/assets/styles/poll-colors.scss'; + +.centered-button-wrapper { + display: flex; + text-align: center; + > * { + margin-left: auto; + margin-right: auto; + } + + .vote-submitted { + color: $votes-yes-color; + font-size: 200%; + overflow: visible; + } +} + +.submit-vote-indicator { + text-align: center; + .mat-spinner { + margin: auto; + } +} diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-cannot-vote-message/poll-cannot-vote-message.component.spec.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-cannot-vote-message/poll-cannot-vote-message.component.spec.ts new file mode 100644 index 0000000000..9ecc177bc8 --- /dev/null +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-cannot-vote-message/poll-cannot-vote-message.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PollCannotVoteMessageComponent } from './poll-cannot-vote-message.component'; + +xdescribe(`PollCannotVoteMessageComponent`, () => { + let component: PollCannotVoteMessageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [PollCannotVoteMessageComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(PollCannotVoteMessageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it(`should create`, () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-cannot-vote-message/poll-cannot-vote-message.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-cannot-vote-message/poll-cannot-vote-message.component.ts new file mode 100644 index 0000000000..be479a7005 --- /dev/null +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-cannot-vote-message/poll-cannot-vote-message.component.ts @@ -0,0 +1,57 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input } from '@angular/core'; +import { debounceTime } from 'rxjs'; +import { OperatorService } from 'src/app/site/services/operator.service'; + +import { BaseMeetingComponent } from '../../../../base/base-meeting.component'; +import { ViewPoll } from '../../../../pages/polls'; +import { ViewUser } from '../../../../view-models/view-user'; +import { VotingService } from '../../services/voting.service'; + +@Component({ + selector: `os-poll-cannot-vote-message`, + templateUrl: `./poll-cannot-vote-message.component.html`, + styleUrls: [`./poll-cannot-vote-message.component.scss`], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class PollCannotVoteMessageComponent extends BaseMeetingComponent { + @Input() + public delegationUser: ViewUser; + + @Input() + public hasAlreadyVoted: boolean; + + @Input() + public isDeliveringVote = false; + + @Input() + public hasDelegations = false; + + @Input() + public poll: ViewPoll; + + public user: ViewUser; + + public get isUserPresent(): boolean { + return this.user?.isPresentInMeeting(); + } + + public constructor(operator: OperatorService, private votingService: VotingService, private cd: ChangeDetectorRef) { + super(); + this.subscriptions.push( + operator.userObservable.pipe(debounceTime(50)).subscribe(user => { + if (user) { + this.user = user; + } + this.cd.markForCheck(); + }) + ); + } + + public getVotingError(user: ViewUser = this.user): string { + return this.votingService.getVotingProhibitionReasonVerbose(this.poll, user) || ``; + } + + public getVotingErrorFromName(errorName: string) { + return this.votingService.getVotingProhibitionReasonVerboseFromName(errorName) || ``; + } +} diff --git a/client/src/app/site/pages/meetings/modules/poll/poll.module.ts b/client/src/app/site/pages/meetings/modules/poll/poll.module.ts index 2922822243..9c687813e3 100644 --- a/client/src/app/site/pages/meetings/modules/poll/poll.module.ts +++ b/client/src/app/site/pages/meetings/modules/poll/poll.module.ts @@ -6,6 +6,7 @@ import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatTooltipModule } from '@angular/material/tooltip'; @@ -19,6 +20,7 @@ import { SearchSelectorModule } from 'src/app/ui/modules/search-selector'; import { ChartComponent } from './components/chart/chart.component'; import { CheckInputComponent } from './components/check-input/check-input.component'; import { EntitledUsersTableComponent } from './components/entitled-users-table/entitled-users-table.component'; +import { PollCannotVoteMessageComponent } from './components/poll-cannot-vote-message/poll-cannot-vote-message.component'; import { PollProgressComponent } from './components/poll-progress/poll-progress.component'; import { SingleOptionChartTableComponent } from './components/single-option-chart-table/single-option-chart-table.component'; import { VotesTableComponent } from './components/votes-table/votes-table.component'; @@ -34,7 +36,8 @@ const COMPONENTS = [ CheckInputComponent, EntitledUsersTableComponent, SingleOptionChartTableComponent, - VotesTableComponent + VotesTableComponent, + PollCannotVoteMessageComponent ]; @NgModule({ @@ -57,6 +60,7 @@ const COMPONENTS = [ ListModule, DirectivesModule, SearchSelectorModule, + MatProgressSpinnerModule, OpenSlidesTranslationModule.forChild() ], exports: [...PIPES, ...MODULES, ...COMPONENTS], diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-vote/topic-poll-vote.component.html b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-vote/topic-poll-vote.component.html deleted file mode 100644 index f588284d84..0000000000 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-vote/topic-poll-vote.component.html +++ /dev/null @@ -1,175 +0,0 @@ - - - - - - -
- - - -
-
-
- - -

- {{ 'Voting right for' | translate }} -  {{ delegation.getFullName() }} -

- - - - -

- {{ 'Available votes' | translate }}: - {{ getVotesAvailable(delegation) }}/{{ poll.max_votes_amount }} - - - ({{ 'At least' | translate }} - {{ poll.min_votes_amount }} - ) - - - - ({{ 'At most' | translate }} - {{ poll.max_votes_per_option }} - {{ 'votes per candidate' | translate }}) - -

- - -
-
-
-
- - {{ option.text }} - - {{ "Text for this option couldn't load." | translate }} -
- - -
- - - {{ action.label | translate }} - -
-
- - - - {{ getErrorInVoteEntry(option.id) }} - - -
- -
-
- - - -
- - -
-
- - -
- -
- -
- {{ 'Voting successful.' | translate }} -
- - -
- -
- {{ 'Delivering vote... Please wait!' | translate }} -
- -
- -
- {{ 'Retrieving vote status... Please wait!' | translate }} -
- - - -
- {{ getVotingError(delegation) | translate }} -
- - -
- {{ getVotingErrorFromName('USER_NOT_PRESENT') | translate }} -
-
-
-
- - -
- -
-
diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-vote/topic-poll-vote.component.scss b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-vote/topic-poll-vote.component.scss deleted file mode 100644 index 2435c6bf1c..0000000000 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-vote/topic-poll-vote.component.scss +++ /dev/null @@ -1,91 +0,0 @@ -@import 'src/assets/styles/poll-colors.scss'; -@import 'src/assets/styles/poll-styles-common.scss'; - -%vote-grid-base { - display: grid; - grid-gap: 10px; - margin: 20px 0; -} - -.topic-vote-delegation { - margin-top: 1em; - - .topic-delegation-title { - font-weight: 500; - } -} - -.yn-grid { - @extend %vote-grid-base; - grid-template-areas: - 'name name' - 'yes no'; -} - -.yna-grid { - @extend %vote-grid-base; - grid-template-areas: - 'name name name' - 'yes no abstain'; -} - -.single-vote-grid { - @extend %vote-grid-base; - grid-template-areas: 'yes name'; - grid-template-columns: min-content auto; -} - -.global-option-grid { - @extend %vote-grid-base; - grid-template-columns: auto auto; -} - -.vote-option-text { - grid-area: name; - display: flex; - span, - i { - margin-top: auto; - margin-bottom: auto; - } -} - -.centered-button-wrapper { - display: flex; - text-align: center; - > * { - margin-left: auto; - margin-right: auto; - } - - .vote-submitted { - color: $votes-yes-color; - font-size: 200%; - overflow: visible; - } -} - -.vote-button { - min-width: 50px; - min-height: 50px; -} - -.vote-input { - width: 120px; -} - -.vote-label { - margin-left: 10px; -} - -.mat-divider-horizontal { - position: initial; -} - -.submit-vote-indicator { - text-align: center; - .mat-mdc-progress-spinner { - display: block; - margin: auto; - } -} diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-vote/topic-poll-vote.component.ts b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-vote/topic-poll-vote.component.ts index 71c89bf87c..8a4c55347e 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-vote/topic-poll-vote.component.ts +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-vote/topic-poll-vote.component.ts @@ -1,10 +1,11 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { UntypedFormControl, Validators } from '@angular/forms'; -import { GlobalVote, PollMethod, PollType, VoteValue } from 'src/app/domain/models/poll'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { marker as _ } from '@colsen1991/ngx-translate-extract-marker'; +import { VoteValue } from 'src/app/domain/models/poll'; import { BasePollVoteComponent, + PollVoteViewSettings, VoteOption -} from 'src/app/site/pages/meetings/modules/poll/base/base-poll-vote.component'; +} from 'src/app/site/pages/meetings/modules/poll/components/base-poll-vote/base-poll-vote.component'; import { ViewOption } from 'src/app/site/pages/meetings/pages/polls'; import { MeetingSettingsService } from 'src/app/site/pages/meetings/services/meeting-settings.service'; import { ViewUser } from 'src/app/site/pages/meetings/view-models/view-user'; @@ -12,52 +13,23 @@ import { PromptService } from 'src/app/ui/modules/prompt-dialog'; import { ViewTopic } from '../../../../view-models'; -const voteOptions = { - Yes: { - vote: `Y`, - css: `voted-yes`, - icon: `thumb_up`, - label: `Yes` - } as VoteOption, - No: { - vote: `N`, - css: `voted-no`, - icon: `thumb_down`, - label: `No` - } as VoteOption, - Abstain: { - vote: `A`, - css: `voted-abstain`, - icon: `trip_origin`, - label: `Abstain` - } as VoteOption -}; - @Component({ selector: `os-topic-poll-vote`, - templateUrl: `./topic-poll-vote.component.html`, - styleUrls: [`./topic-poll-vote.component.scss`], + templateUrl: `../../../../../../../../modules/poll/components/base-poll-vote/base-poll-vote.component.html`, + styleUrls: [`../../../../../../../../modules/poll/components/base-poll-vote/base-poll-vote.component.scss`], changeDetection: ChangeDetectionStrategy.OnPush }) -export class TopicPollVoteComponent extends BasePollVoteComponent implements OnInit { - public TopicPollMethod = PollMethod; - public PollType = PollType; - public voteActions: VoteOption[] = []; - public formControlMap: { [optionId: number]: UntypedFormControl } = {}; +export class TopicPollVoteComponent extends BasePollVoteComponent { + public override readonly settings: PollVoteViewSettings = { + hideGlobalOptions: true + }; - public get minVotes(): number { - return this.poll.min_votes_amount; - } + public override readonly noDataLabel = _(`Text for this option couldn't load.`); public constructor(private promptService: PromptService, meetingSettingsService: MeetingSettingsService) { super(meetingSettingsService); } - public ngOnInit(): void { - this.defineVoteOptions(); - this.cd.markForCheck(); - } - public getActionButtonClass(actions: VoteOption, option: ViewOption, user: ViewUser = this.user): string { if ( this.voteRequestData[user?.id]?.value[option.id] === actions.vote || @@ -68,111 +40,7 @@ export class TopicPollVoteComponent extends BasePollVoteComponent imp return ``; } - public getGlobalYesClass(user: ViewUser = this.user): string { - if (this.voteRequestData[user.id]?.value === `Y`) { - return `voted-yes`; - } - return ``; - } - - public getGlobalAbstainClass(user: ViewUser = this.user): string { - if (this.voteRequestData[user.id]?.value === `A`) { - return `voted-abstain`; - } - return ``; - } - - public getGlobalNoClass(user?: ViewUser): string { - if (!user) { - if (!this.user) { - return ``; - } - user = this.user; - } - if (this.voteRequestData[user.id]?.value === `N`) { - return `voted-no`; - } - return ``; - } - - private defineVoteOptions(): void { - this.voteActions = []; - if (this.poll) { - if (this.poll.isMethodN) { - this.voteActions.push(voteOptions.No); - } else { - this.voteActions.push(voteOptions.Yes); - - if (!this.poll.isMethodY) { - this.voteActions.push(voteOptions.No); - } - - if (this.poll.isMethodYNA) { - this.voteActions.push(voteOptions.Abstain); - } - } - } - } - - public getFormControl(optionId: number): UntypedFormControl { - if (!this.formControlMap[optionId]) { - this.formControlMap[optionId] = new UntypedFormControl(0, [ - Validators.required, - Validators.min(0), - Validators.max(this.poll.max_votes_per_option) - ]); - } - return this.formControlMap[optionId]; - } - - public isErrorInVoteEntry(): boolean { - for (const key in this.formControlMap) { - if (this.formControlMap.hasOwnProperty(key) && this.formControlMap[key].invalid) { - return true; - } - } - return false; - } - - public getErrorInVoteEntry(optionId: number): string { - if (this.formControlMap[optionId].hasError(`required`)) { - return this.translate.instant(`This is not a number.`); - } else if (this.formControlMap[optionId].hasError(`min`)) { - return this.translate.instant(`Negative votes are not allowed.`); - } else if (this.formControlMap[optionId].hasError(`max`)) { - return this.translate.instant(`Too many votes on one option.`); - } - return ``; - } - - public getVotesCount(user: ViewUser = this.user): number { - if (this.voteRequestData[user?.id]) { - if (this.poll.isMethodY && this.poll.max_votes_per_option > 1 && !this.isGlobalOptionSelected(user)) { - return Object.keys(this.voteRequestData[user.id].value) - .map(key => parseInt(this.voteRequestData[user.id].value[key], 10)) - .reduce((a, b) => a + b, 0); - } else { - return Object.keys(this.voteRequestData[user.id].value).filter( - key => this.voteRequestData[user.id].value[key] - ).length; - } - } - return 0; - } - - public getVotesAvailable(user: ViewUser = this.user): number | string { - if (this.isGlobalOptionSelected()) { - return `-`; - } - return this.poll.max_votes_amount - this.getVotesCount(user); - } - - private isGlobalOptionSelected(user: ViewUser = this.user): boolean { - const value = this.voteRequestData[user.id]?.value; - return value === `Y` || value === `N` || value === `A`; - } - - public async submitVote(user: ViewUser = this.user): Promise { + public override async submitVote(user: ViewUser = this.user): Promise { const value = this.voteRequestData[user.id].value; if (this.poll.isMethodY && this.poll.max_votes_per_option > 1 && this.isErrorInVoteEntry()) { this.raiseError(this.translate.instant(`There is an error in your vote.`)); @@ -182,15 +50,7 @@ export class TopicPollVoteComponent extends BasePollVoteComponent imp const content = this.translate.instant(`Your decision cannot be changed afterwards.`); const confirmed = await this.promptService.open(title, content); if (confirmed) { - this.deliveringVote[user.id] = true; - this.cd.markForCheck(); - - const votePayload = { - value: value, - user_id: user.id - }; - - await this.sendVote(user.id, votePayload); + await super.submitVote(user, value); } } @@ -211,7 +71,11 @@ export class TopicPollVoteComponent extends BasePollVoteComponent imp } } - public saveSingleVoteMethodYNOrYNA(optionId: number, vote: VoteValue, user: ViewUser = this.user): void { + public override shouldStrikeOptionText(_option: ViewOption, _user: ViewUser): boolean { + return false; + } + + private saveSingleVoteMethodYNOrYNA(optionId: number, vote: VoteValue, user: ViewUser = this.user): void { if (this.voteRequestData[user.id].value[optionId] && this.voteRequestData[user.id].value[optionId] === vote) { delete (this.voteRequestData[user.id] as any).value[optionId]; } else { @@ -224,7 +88,7 @@ export class TopicPollVoteComponent extends BasePollVoteComponent imp } } - public saveSingleVoteMethodYOrN(optionId: number, vote: VoteValue, user: ViewUser = this.user): void { + private saveSingleVoteMethodYOrN(optionId: number, vote: VoteValue, user: ViewUser = this.user): void { const maxVotesAmount = this.poll.max_votes_amount; const tmpVoteRequest = this.getTMPVoteRequestYOrN(maxVotesAmount, optionId, user); @@ -264,108 +128,4 @@ export class TopicPollVoteComponent extends BasePollVoteComponent imp return o; }, {}); } - - public saveMultipleVotes(optionId: number, event: any, user: ViewUser = this.user): void { - let vote = parseInt(event.target.value, 10); - - if (isNaN(vote) || vote > this.poll.max_votes_per_option || vote < 0) { - vote = 0; - } - - if (!this.voteRequestData[user.id]) { - throw new Error(`The user for your voting request does not exist`); - } - - if (this.isGlobalOptionSelected(user)) { - delete this.voteRequestData[user.id].value; - } - - if (this.poll.isMethodY && this.poll.max_votes_per_option > 1) { - this.saveMultipleVotesMultiVoteMethodY(optionId, vote, user); - } - } - - private saveMultipleVotesMultiVoteMethodY(optionId: number, vote: number, user: ViewUser = this.user): void { - // Another option is not expected here - const maxVotesAmount = this.poll.max_votes_amount; - const tmpVoteRequest = this.getTmpVoteRequestMultipleVotes(optionId, vote, user); - - // check if you can still vote - const countedVotes = Object.keys(tmpVoteRequest) - .map(key => parseInt(tmpVoteRequest[key], 10)) - .reduce((a, b) => a + b, 0); - if (countedVotes <= maxVotesAmount) { - this.voteRequestData[user.id].value = tmpVoteRequest; - - // if you have no options anymore, try to send - if (this.getVotesCount(user) === maxVotesAmount && !this.isErrorInVoteEntry()) { - this.submitVote(user); - } - } else { - this.raiseError( - this.translate.instant(`You reached the maximum amount of votes. Deselect one option first.`) - ); - this.formControlMap[optionId].setValue(this.voteRequestData[user.id].value[optionId]); - } - } - - private getTmpVoteRequestMultipleVotes( - optionId: number, - vote: number, - user: ViewUser = this.user - ): { [option_id: number]: number } { - const maxVotesAmount = this.poll.max_votes_amount; - const maxVotesPerOption = this.poll.max_votes_per_option; - return this.poll.options - .map(option => option.id) - .reduce((output, next_id) => { - output[next_id] = this.voteRequestData[user.id].value[next_id]; - output[next_id] = output[next_id] ? output[next_id] : 0; - if (next_id === optionId) { - if (vote > Math.min(maxVotesPerOption, maxVotesAmount)) { - output[next_id] = Math.min(maxVotesPerOption, maxVotesAmount); - } else if (vote >= 0) { - output[next_id] = vote; - } - } - return output; - }, {}); - } - - public saveGlobalVote(globalVote: GlobalVote, user: ViewUser = this.user): void { - if (this.voteRequestData[user.id].value && this.voteRequestData[user.id].value === globalVote) { - this.voteRequestData[user.id].value = {}; - if (this.poll.isMethodY && this.poll.max_votes_per_option > 1) { - this.enableInputs(); - } - } else { - this.voteRequestData[user.id].value = globalVote; - if (this.poll.isMethodY && this.poll.max_votes_per_option > 1) { - this.disableAndResetInputs(); - } - this.submitVote(user); - } - } - - protected override updatePoll(): void { - super.updatePoll(); - this.defineVoteOptions(); - } - - private enableInputs(): void { - for (const key in this.formControlMap) { - if (this.formControlMap.hasOwnProperty(key)) { - this.formControlMap[key].enable(); - } - } - } - - private disableAndResetInputs(): void { - for (const key in this.formControlMap) { - if (this.formControlMap.hasOwnProperty(key)) { - this.formControlMap[key].setValue(0); - this.formControlMap[key].disable(); - } - } - } } diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/topic-poll.module.ts b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/topic-poll.module.ts index bb0a3d6c8a..dfa1a29418 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/topic-poll.module.ts +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/topic-poll.module.ts @@ -19,6 +19,7 @@ import { PollService } from 'src/app/site/pages/meetings/modules/poll/services/p import { DirectivesModule } from 'src/app/ui/directives'; import { ChoiceDialogModule } from 'src/app/ui/modules/choice-dialog'; import { CommaSeparatedListingModule } from 'src/app/ui/modules/comma-separated-listing'; +import { CustomIconModule } from 'src/app/ui/modules/custom-icon'; import { IconContainerModule } from 'src/app/ui/modules/icon-container'; import { SearchSelectorModule } from 'src/app/ui/modules/search-selector'; import { SortingListModule } from 'src/app/ui/modules/sorting/modules'; @@ -43,6 +44,7 @@ import { TopicPollServiceModule } from './services/topic-poll-service.module'; TopicPollFormComponent ], imports: [ + CustomIconModule, CommonModule, CommaSeparatedListingModule, TopicPollServiceModule, diff --git a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/assignment-poll.module.ts b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/assignment-poll.module.ts index 31b1d6393f..01c3d7721f 100644 --- a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/assignment-poll.module.ts +++ b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/assignment-poll.module.ts @@ -20,6 +20,7 @@ import { PollService } from 'src/app/site/pages/meetings/modules/poll/services/p import { DirectivesModule } from 'src/app/ui/directives'; import { ChoiceDialogModule } from 'src/app/ui/modules/choice-dialog'; import { CommaSeparatedListingModule } from 'src/app/ui/modules/comma-separated-listing'; +import { CustomIconModule } from 'src/app/ui/modules/custom-icon'; import { ExpandableContentWrapperModule } from 'src/app/ui/modules/expandable-content-wrapper'; import { IconContainerModule } from 'src/app/ui/modules/icon-container'; import { SearchSelectorModule } from 'src/app/ui/modules/search-selector'; @@ -46,6 +47,7 @@ const COMPONENTS = [ declarations: [...COMPONENTS, AssignmentPollDialogComponent, AssignmentPollFormComponent], exports: [...COMPONENTS, PollModule, AssignmentPollServiceModule], imports: [ + CustomIconModule, CommonModule, CommaSeparatedListingModule, AssignmentPollServiceModule, diff --git a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-vote/assignment-poll-vote.component.html b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-vote/assignment-poll-vote.component.html deleted file mode 100644 index a8f454eef0..0000000000 --- a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-vote/assignment-poll-vote.component.html +++ /dev/null @@ -1,239 +0,0 @@ - - - - - - -
- - - -
-
-
- - -

- {{ 'Voting right for' | translate }} -  {{ delegation.getFullName() }} -

- - - - -

- {{ pollHint }} -

- - -

- {{ 'Available votes' | translate }}: - {{ getVotesAvailable(delegation) }}/{{ poll.max_votes_amount }} - - - ({{ 'At least' | translate }} - {{ poll.min_votes_amount }} - ) - - - - ({{ 'At most' | translate }} - {{ poll.max_votes_per_option }} - {{ 'votes per candidate' | translate }}) - -

- - -
-
-
-
- - {{ option.content_object.short_name }} -
- {{ option.content_object.getLevelAndNumber() }} -
-
- {{ unknownUserLabel | translate }} -
-
- - {{ option.content_object.getTitle() | translate }} - -
- - -
- - - {{ action.label | translate }} - -
-
- - - - {{ getErrorInVoteEntry(option.id) }} - - -
- -
-
- - - - -
-
- - - {{ PollPropertyVerbose.global_yes | translate }} - -
- -
- - - {{ PollPropertyVerbose.global_no | translate }} - -
- -
- - - {{ PollPropertyVerbose.global_abstain | translate }} - -
-
-
- - - -
- - -
-
- - -
- -
- -
- {{ 'Voting successful.' | translate }} -
- - -
- -
- {{ 'Delivering vote... Please wait!' | translate }} -
- -
- -
- {{ 'Retrieving vote status... Please wait!' | translate }} -
- - - -
- {{ getVotingError(delegation) | translate }} -
- - -
- {{ getVotingErrorFromName('USER_NOT_PRESENT') | translate }} -
-
-
-
- - -
- -
-
diff --git a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-vote/assignment-poll-vote.component.scss b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-vote/assignment-poll-vote.component.scss deleted file mode 100644 index 00255615e2..0000000000 --- a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-vote/assignment-poll-vote.component.scss +++ /dev/null @@ -1,91 +0,0 @@ -@import 'src/assets/styles/poll-colors.scss'; -@import 'src/assets/styles/poll-styles-common.scss'; - -%vote-grid-base { - display: grid; - grid-gap: 10px; - margin: 20px 0; -} - -.assignment-vote-delegation { - margin-top: 1em; - - .assignment-delegation-title { - font-weight: 500; - } -} - -.yn-grid { - @extend %vote-grid-base; - grid-template-areas: - 'name name' - 'yes no'; -} - -.yna-grid { - @extend %vote-grid-base; - grid-template-areas: - 'name name name' - 'yes no abstain'; -} - -.single-vote-grid { - @extend %vote-grid-base; - grid-template-areas: 'yes name'; - grid-template-columns: min-content auto; -} - -.global-option-grid { - @extend %vote-grid-base; - grid-template-columns: auto auto; -} - -.vote-candidate-name { - grid-area: name; - display: flex; - span, - i { - margin-top: auto; - margin-bottom: auto; - } -} - -.centered-button-wrapper { - display: flex; - text-align: center; - > * { - margin-left: auto; - margin-right: auto; - } - - .vote-submitted { - color: $votes-yes-color; - font-size: 200%; - overflow: visible; - } -} - -.vote-button { - min-width: 50px; - min-height: 50px; -} - -.vote-input { - width: 120px; -} - -.vote-label { - margin-left: 10px; -} - -.mat-divider-horizontal { - position: initial; -} - -.submit-vote-indicator { - text-align: center; - .mat-mdc-progress-spinner { - display: block; - margin: auto; - } -} diff --git a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-vote/assignment-poll-vote.component.ts b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-vote/assignment-poll-vote.component.ts index 5d45cb077c..11f98d923b 100644 --- a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-vote/assignment-poll-vote.component.ts +++ b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-vote/assignment-poll-vote.component.ts @@ -1,11 +1,11 @@ -import { Component, OnInit } from '@angular/core'; -import { UntypedFormControl, Validators } from '@angular/forms'; -import { GlobalVote, PollMethod, PollType } from 'src/app/domain/models/poll/poll-constants'; +import { Component } from '@angular/core'; +import { marker as _ } from '@colsen1991/ngx-translate-extract-marker'; +import { PollMethod } from 'src/app/domain/models/poll/poll-constants'; import { VoteValue } from 'src/app/domain/models/poll/vote-constants'; import { BasePollVoteComponent, VoteOption -} from 'src/app/site/pages/meetings/modules/poll/base/base-poll-vote.component'; +} from 'src/app/site/pages/meetings/modules/poll/components/base-poll-vote/base-poll-vote.component'; import { ViewAssignment } from 'src/app/site/pages/meetings/pages/assignments'; import { ViewOption } from 'src/app/site/pages/meetings/pages/polls'; import { MeetingSettingsService } from 'src/app/site/pages/meetings/services/meeting-settings.service'; @@ -14,53 +14,25 @@ import { PromptService } from 'src/app/ui/modules/prompt-dialog'; import { UnknownUserLabel } from '../../services/assignment-poll.service'; -const voteOptions: { - Yes: VoteOption; - No: VoteOption; - Abstain: VoteOption; -} = { - Yes: { - vote: `Y`, - css: `voted-yes`, - icon: `thumb_up`, - label: `Yes` - } as VoteOption, - No: { - vote: `N`, - css: `voted-no`, - icon: `thumb_down`, - label: `No` - } as VoteOption, - Abstain: { - vote: `A`, - css: `voted-abstain`, - icon: `trip_origin`, - label: `Abstain` - } as VoteOption -}; - @Component({ selector: `os-assignment-poll-vote`, - templateUrl: `./assignment-poll-vote.component.html`, - styleUrls: [`./assignment-poll-vote.component.scss`] + templateUrl: `../../../../../../modules/poll/components/base-poll-vote/base-poll-vote.component.html`, + styleUrls: [`../../../../../../modules/poll/components/base-poll-vote/base-poll-vote.component.scss`] }) -export class AssignmentPollVoteComponent extends BasePollVoteComponent implements OnInit { +export class AssignmentPollVoteComponent extends BasePollVoteComponent { public unknownUserLabel = UnknownUserLabel; public AssignmentPollMethod = PollMethod; - public PollType = PollType; - public voteActions: VoteOption[] = []; - public formControlMap: { [optionId: number]: UntypedFormControl } = {}; - public get pollHint(): string | null { + public override get pollHint(): string | null { if (this.poll?.content_object) { return this.poll.content_object!.default_poll_description; } return null; } - public get minVotes(): number { - return this.poll.min_votes_amount; - } + public override readonly maxVotesPerOptionSuffix = _(`votes per candidate`); + + public override readonly optionPluralLabel: string = _(`Candidates`); private get assignment(): ViewAssignment { return this.poll.content_object; @@ -74,11 +46,6 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent 1 && !this.isGlobalOptionSelected(user)) { - return Object.keys(this.voteRequestData[user.id].value) - .map(key => parseInt(this.voteRequestData[user.id].value[+key] as string, 10)) - .reduce((a, b) => a + b, 0); - } else { - return Object.keys(this.voteRequestData[user.id].value).filter( - key => this.voteRequestData[user.id].value[+key] - ).length; - } - } - return 0; - } - - public getVotesAvailable(user: ViewUser = this.user): number | string { - if (this.isGlobalOptionSelected()) { - return `-`; - } - return this.poll.max_votes_amount - this.getVotesCount(user); - } - - private isGlobalOptionSelected(user: ViewUser = this.user): boolean { - const value = this.voteRequestData[user.id]?.value; - return value === `Y` || value === `N` || value === `A`; - } - - public async submitVote(user: ViewUser = this.user): Promise { + public override async submitVote(user: ViewUser = this.user): Promise { const value = this.voteRequestData[user.id].value; if (this.poll.isMethodY && this.poll.max_votes_per_option > 1 && this.isErrorInVoteEntry()) { this.raiseError(this.translate.instant(`There is an error in your vote.`)); @@ -209,15 +72,7 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent this.poll.max_votes_per_option || vote < 0) { - vote = 0; - } - - if (!this.voteRequestData[user.id]) { - throw new Error(`The user for your voting request does not exist`); - } - - if (this.isGlobalOptionSelected(user)) { - this.voteRequestData[user.id].value = {}; - } - - if (this.poll.isMethodY && this.poll.max_votes_per_option > 1) { - // Another option is not expected here - const maxVotesAmount = this.poll.max_votes_amount; - const tmpVoteRequest = this.getTmpVoteRequestMultipleVotes(optionId, vote, user); - - // check if you can still vote - const countedVotes = Object.keys(tmpVoteRequest) - .map(key => tmpVoteRequest[+key]) - .reduce((a, b) => a + b, 0); - if (countedVotes <= maxVotesAmount) { - this.voteRequestData[user.id].value = tmpVoteRequest; - - // if you have no options anymore, try to send - if (this.getVotesCount(user) === maxVotesAmount && !this.isErrorInVoteEntry()) { - this.submitVote(user); - } - } else { - this.raiseError( - this.translate.instant(`You reached the maximum amount of votes. Deselect somebody first.`) - ); - this.formControlMap[optionId].setValue(this.voteRequestData[user.id].value[optionId]); - } - } - } - - private getTmpVoteRequestMultipleVotes( - optionId: number, - vote: number, - user: ViewUser = this.user - ): { [option_id: number]: number } { - const maxVotesAmount = this.poll.max_votes_amount; - const maxVotesPerOption = this.poll.max_votes_per_option; - return this.poll.options - .map(option => option.id) - .reduce((output: any, next_id) => { - output[next_id] = this.voteRequestData[user.id].value[next_id]; - output[next_id] = output[next_id] ? output[next_id] : 0; - if (next_id === optionId) { - if (vote > Math.min(maxVotesPerOption, maxVotesAmount)) { - output[next_id] = Math.min(maxVotesPerOption, maxVotesAmount); - } else if (vote >= 0) { - output[next_id] = vote; - } - } - return output; - }, {}); - } - - public saveGlobalVote(globalVote: GlobalVote, user: ViewUser = this.user): void { - if (this.voteRequestData[user.id].value && this.voteRequestData[user.id].value === globalVote) { - this.voteRequestData[user.id].value = {}; - if (this.poll.isMethodY && this.poll.max_votes_per_option > 1) { - this.enableInputs(); - } - } else { - this.voteRequestData[user.id].value = globalVote; - if (this.poll.isMethodY && this.poll.max_votes_per_option > 1) { - this.disableAndResetInputs(); - } - this.submitVote(user); - } - } - - protected override updatePoll(): void { - super.updatePoll(); - this.defineVoteOptions(); - } - - private enableInputs(): void { - for (const key in this.formControlMap) { - if (this.formControlMap.hasOwnProperty(key)) { - this.formControlMap[key].enable(); - } - } - } - - private disableAndResetInputs(): void { - for (const key in this.formControlMap) { - if (this.formControlMap.hasOwnProperty(key)) { - this.formControlMap[key].setValue(0); - this.formControlMap[key].disable(); - } + public override shouldStrikeOptionText(option: ViewOption, user: ViewUser = this.user): boolean { + if (this.poll.pollmethod === PollMethod.N) { + return !!this.voteRequestData[user.id].value[option.id]; } + return false; } } diff --git a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-vote/motion-poll-vote.component.html b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-vote/motion-poll-vote.component.html deleted file mode 100644 index 4613180285..0000000000 --- a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-vote/motion-poll-vote.component.html +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - -
- - - -
-
-
- - -

- {{ 'Voting right for' | translate }} -  {{ delegation.getFullName() }} -

- - -
- - -
- - {{ option.label | translate }} -
-
-
- - - - -
- -
- {{ 'Delivering vote... Please wait!' | translate }} -
-
-
- - - -
-
- -
- {{ 'Voting successful.' | translate }} -
-
- -
- -
- {{ 'Retrieving vote status... Please wait!' | translate }} -
- - - -
- {{ getVotingError(delegation) | translate }} -
-
- {{ getVotingErrorFromName('USER_NOT_PRESENT') | translate }} -
-
-
diff --git a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-vote/motion-poll-vote.component.scss b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-vote/motion-poll-vote.component.scss deleted file mode 100644 index 01a52b61fb..0000000000 --- a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-vote/motion-poll-vote.component.scss +++ /dev/null @@ -1,56 +0,0 @@ -@import 'src/assets/styles/poll-colors.scss'; -@import 'src/assets/styles/poll-styles-common.scss'; - -.vote-button-grid { - display: grid; - grid-gap: 20px; - margin-top: 2em; - grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); -} - -.motion-vote-delegation { - margin-top: 1em; - - .motion-delegation-title { - font-weight: 500; - } -} - -.submit-vote-indicator { - margin-top: 1em; - text-align: center; - .mat-mdc-progress-spinner { - display: block; - margin: auto; - } -} - -.vote-button { - display: inline-grid; - grid-gap: 1em; - margin: auto; - - .vote-label { - text-align: center; - } -} - -.mat-divider-horizontal { - position: initial; -} - -.user-has-voted { - display: flex; - text-align: center; - > * { - margin-top: 1em; - margin-left: auto; - margin-right: auto; - } - - .vote-submitted { - color: $votes-yes-color; - font-size: 200%; - overflow: visible; - } -} diff --git a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-vote/motion-poll-vote.component.ts b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-vote/motion-poll-vote.component.ts index 8182cae1db..24ef8d59dc 100644 --- a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-vote/motion-poll-vote.component.ts +++ b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-vote/motion-poll-vote.component.ts @@ -1,57 +1,41 @@ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { Id } from 'src/app/domain/definitions/key-types'; import { VoteValue } from 'src/app/domain/models/poll/vote-constants'; import { BasePollVoteComponent, VoteOption -} from 'src/app/site/pages/meetings/modules/poll/base/base-poll-vote.component'; +} from 'src/app/site/pages/meetings/modules/poll/components/base-poll-vote/base-poll-vote.component'; import { MeetingSettingsService } from 'src/app/site/pages/meetings/services/meeting-settings.service'; import { ViewUser } from 'src/app/site/pages/meetings/view-models/view-user'; import { PromptService } from 'src/app/ui/modules/prompt-dialog'; +import { ViewOption } from '../../../../../polls'; + @Component({ selector: `os-motion-poll-vote`, - templateUrl: `./motion-poll-vote.component.html`, - styleUrls: [`./motion-poll-vote.component.scss`] + templateUrl: `../../../../../../modules/poll/components/base-poll-vote/base-poll-vote.component.html`, + styleUrls: [`../../../../../../modules/poll/components/base-poll-vote/base-poll-vote.component.scss`] }) -export class MotionPollVoteComponent extends BasePollVoteComponent implements OnInit { - public voteOptions: VoteOption[] = [ - { - vote: `Y`, - css: `voted-yes`, - icon: `thumb_up`, - label: `Yes` - }, - { - vote: `N`, - css: `voted-no`, - icon: `thumb_down`, - label: `No` - }, - { - vote: `A`, - css: `voted-abstain`, - icon: `trip_origin`, - label: `Abstain` - } - ]; +export class MotionPollVoteComponent extends BasePollVoteComponent { + public override readonly settings = { + hideLeftoverVotes: true, + hideGlobalOptions: true, + hideSendNow: true, + isSplitSingleOption: true + }; public constructor(private promptService: PromptService, meetingSettingsService: MeetingSettingsService) { super(meetingSettingsService); } - public ngOnInit(): void { - this.cd.markForCheck(); - } - - public getActionButtonClass(voteOption: VoteOption, user: ViewUser = this.user): string { + public getActionButtonClass(voteOption: VoteOption, option: ViewOption, user: ViewUser = this.user): string { if (this.voteRequestData[user?.id]?.value === voteOption.vote) { return voteOption.css!; } return ``; } - public async saveVote(vote: VoteValue, optionId: Id, user: ViewUser = this.user): Promise { + public async saveSingleVote(optionId: Id, vote: VoteValue, user: ViewUser = this.user): Promise { if (!this.voteRequestData[user?.id]) { return; } @@ -62,14 +46,11 @@ export class MotionPollVoteComponent extends BasePollVoteComponent implements On const confirmed = await this.promptService.open(title, content); if (confirmed) { - this.deliveringVote[user.id] = true; - this.cd.markForCheck(); - - const votePayload = { - value: { [optionId]: vote }, - user_id: user.id - }; - await this.sendVote(user.id, votePayload); + await super.submitVote(user, { [optionId]: vote }); } } + + public override shouldStrikeOptionText(_option: ViewOption, _user: ViewUser): boolean { + return false; + } } diff --git a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/motion-poll.module.ts b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/motion-poll.module.ts index 051c2d3510..19d6a41284 100644 --- a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/motion-poll.module.ts +++ b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/motion-poll.module.ts @@ -21,6 +21,7 @@ import { PollModule } from 'src/app/site/pages/meetings/modules/poll'; import { PollService } from 'src/app/site/pages/meetings/modules/poll/services/poll.service'; import { DirectivesModule } from 'src/app/ui/directives'; import { CommaSeparatedListingModule } from 'src/app/ui/modules/comma-separated-listing'; +import { CustomIconModule } from 'src/app/ui/modules/custom-icon'; import { IconContainerModule } from 'src/app/ui/modules/icon-container'; import { SearchSelectorModule } from 'src/app/ui/modules/search-selector'; import { PipesModule } from 'src/app/ui/pipes'; @@ -45,6 +46,7 @@ const MODULES = [MotionPollServiceModule]; @NgModule({ imports: [ ...MODULES, + CustomIconModule, CommonModule, CommaSeparatedListingModule, RouterModule, diff --git a/client/src/app/ui/modules/custom-icon/components/custom-icon/custom-icon.component.html b/client/src/app/ui/modules/custom-icon/components/custom-icon/custom-icon.component.html new file mode 100644 index 0000000000..0eba54fb4e --- /dev/null +++ b/client/src/app/ui/modules/custom-icon/components/custom-icon/custom-icon.component.html @@ -0,0 +1,3 @@ + + + diff --git a/client/src/app/ui/modules/custom-icon/components/custom-icon/custom-icon.component.scss b/client/src/app/ui/modules/custom-icon/components/custom-icon/custom-icon.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/client/src/app/ui/modules/custom-icon/components/custom-icon/custom-icon.component.spec.ts b/client/src/app/ui/modules/custom-icon/components/custom-icon/custom-icon.component.spec.ts new file mode 100644 index 0000000000..61d4ebbd90 --- /dev/null +++ b/client/src/app/ui/modules/custom-icon/components/custom-icon/custom-icon.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CustomIconComponent } from './custom-icon.component'; + +xdescribe(`CustomIconComponent`, () => { + let component: CustomIconComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [CustomIconComponent] + }); + fixture = TestBed.createComponent(CustomIconComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it(`should create`, () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/ui/modules/custom-icon/components/custom-icon/custom-icon.component.ts b/client/src/app/ui/modules/custom-icon/components/custom-icon/custom-icon.component.ts new file mode 100644 index 0000000000..9dc7addbe3 --- /dev/null +++ b/client/src/app/ui/modules/custom-icon/components/custom-icon/custom-icon.component.ts @@ -0,0 +1,24 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +import { CustomIcon } from '../../definitions'; + +@Component({ + selector: `os-custom-icon`, + templateUrl: `./custom-icon.component.html`, + styleUrls: [`./custom-icon.component.scss`], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class CustomIconComponent { + @Input() + public customIcon!: CustomIcon; + + @Input() + public sizeInPx = 24; + + public get style(): { [ley: string]: any } { + return { + height: `${this.sizeInPx}px`, + width: `${this.sizeInPx}px` + }; + } +} diff --git a/client/src/app/ui/modules/custom-icon/custom-icon.module.ts b/client/src/app/ui/modules/custom-icon/custom-icon.module.ts new file mode 100644 index 0000000000..45f0243ea1 --- /dev/null +++ b/client/src/app/ui/modules/custom-icon/custom-icon.module.ts @@ -0,0 +1,11 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { CustomIconComponent } from './components/custom-icon/custom-icon.component'; + +@NgModule({ + declarations: [CustomIconComponent], + imports: [CommonModule], + exports: [CustomIconComponent] +}) +export class CustomIconModule {} diff --git a/client/src/app/ui/modules/custom-icon/definitions/index.ts b/client/src/app/ui/modules/custom-icon/definitions/index.ts new file mode 100644 index 0000000000..5fa9108d39 --- /dev/null +++ b/client/src/app/ui/modules/custom-icon/definitions/index.ts @@ -0,0 +1,7 @@ +/** + * Enum containing svg sources for all custom icons drawn from svg files + * viewBox will be interpreted as "0 0 24 24" + */ +export enum CustomIcon { + DRAWN_CROSS = `assets/img/drawn_cross.svg#group-R5` +} diff --git a/client/src/app/ui/modules/custom-icon/index.ts b/client/src/app/ui/modules/custom-icon/index.ts new file mode 100644 index 0000000000..dd4a4e772d --- /dev/null +++ b/client/src/app/ui/modules/custom-icon/index.ts @@ -0,0 +1 @@ +export * from './custom-icon.module'; diff --git a/client/src/assets/img/drawn_cross.svg b/client/src/assets/img/drawn_cross.svg new file mode 100644 index 0000000000..0b4e2d3a32 --- /dev/null +++ b/client/src/assets/img/drawn_cross.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + diff --git a/client/src/assets/styles/global-components-style.scss b/client/src/assets/styles/global-components-style.scss index 339c0c290f..f97386bbef 100644 --- a/client/src/assets/styles/global-components-style.scss +++ b/client/src/assets/styles/global-components-style.scss @@ -30,6 +30,16 @@ } } + .accentuated-background, .accentuated-background button { + background-color: if($is-dark-theme, lighten(mat.get-color-from-palette($background, background), 4%), darken(mat.get-color-from-palette($background, background), 4%)); + } + + mat-card { + .accentuated-background, .accentuated-background button { + background-color: if($is-dark-theme, lighten(mat.get-color-from-palette($background, card), 4%), darken(mat.get-color-from-palette($background, card), 4%)); + } + } + .anchor-button { color: mat.get-color-from-palette($foreground, text) !important; } diff --git a/client/src/assets/styles/poll-styles-common.scss b/client/src/assets/styles/poll-styles-common.scss index b570f91617..7b411492a6 100644 --- a/client/src/assets/styles/poll-styles-common.scss +++ b/client/src/assets/styles/poll-styles-common.scss @@ -13,18 +13,18 @@ } .voted-yes { - background-color: $votes-yes-color; - color: $vote-active-color; + color: $votes-yes-color; + opacity: 1 !important; } .voted-no { - background-color: $votes-no-color; - color: $vote-active-color; + color: $votes-no-color; + opacity: 1 !important; } .voted-abstain { - background-color: $votes-abstain-color; - color: $vote-active-color; + color: $votes-abstain-color; + opacity: 1 !important; } .start-poll-button { diff --git a/client/src/meta b/client/src/meta index d5ea426ad3..9999cca6fe 160000 --- a/client/src/meta +++ b/client/src/meta @@ -1 +1 @@ -Subproject commit d5ea426ad345c139ec2d00bbf656b80a73f5d9c7 +Subproject commit 9999cca6fef93b88c0b079cba4c6af436d334f02