diff --git a/client/src/app/domain/models/poll/poll-config-approval.ts b/client/src/app/domain/models/poll/poll-config-approval.ts index 9a4d634a004..0816a3e7dff 100644 --- a/client/src/app/domain/models/poll/poll-config-approval.ts +++ b/client/src/app/domain/models/poll/poll-config-approval.ts @@ -1,4 +1,5 @@ import { HasMeetingId } from '../../interfaces/has-meeting-id'; +import { PollRequiredMajority } from '.'; import { BasePollConfigModel } from './base-poll-config'; import { BaseOnehundredPercentBase } from './poll-config-types'; @@ -9,6 +10,7 @@ export class PollConfigApproval extends BasePollConfigModel public allow_abstain!: boolean; public onehundred_percent_base!: ApprovalOnehundredPercentBase; + public required_majority!: PollRequiredMajority; public constructor(input?: Partial) { super(PollConfigApproval.COLLECTION, input); @@ -18,7 +20,8 @@ export class PollConfigApproval extends BasePollConfigModel `id`, `poll_id`, `allow_abstain`, - `onehundred_percent_base` + `onehundred_percent_base`, + `required_majority` ]; } diff --git a/client/src/app/domain/models/poll/poll-config-rating-approval.ts b/client/src/app/domain/models/poll/poll-config-rating-approval.ts index 290822b1fb5..cce1704004b 100644 --- a/client/src/app/domain/models/poll/poll-config-rating-approval.ts +++ b/client/src/app/domain/models/poll/poll-config-rating-approval.ts @@ -1,4 +1,5 @@ import { HasMeetingId } from '../../interfaces/has-meeting-id'; +import { PollRequiredMajority } from '.'; import { BasePollConfigModel } from './base-poll-config'; import { BaseOnehundredPercentBase } from './poll-config-types'; @@ -12,6 +13,7 @@ export class PollConfigRatingApproval extends BasePollConfigModel) { super(PollConfigRatingScore.COLLECTION, input); @@ -26,7 +28,8 @@ export class PollConfigRatingScore extends BasePollConfigModel { + this.unsafeViewModelListSubject.pipe(throttleTime(20)).subscribe(models => { if (models) { this.updateViewModelListSubject(models); } diff --git a/client/src/app/gateways/repositories/polls/poll-repository.service/poll-repository.service.ts b/client/src/app/gateways/repositories/polls/poll-repository.service/poll-repository.service.ts index 1771b643566..aff7fa7a8ff 100644 --- a/client/src/app/gateways/repositories/polls/poll-repository.service/poll-repository.service.ts +++ b/client/src/app/gateways/repositories/polls/poll-repository.service/poll-repository.service.ts @@ -69,7 +69,7 @@ export class PollRepositoryService extends BaseMeetingRelatedRepository { - if (poll.config.METHOD_NAME === payload.method) { + if (poll.config.method === payload.method) { delete payload[`method`]; } @@ -77,7 +77,21 @@ export class PollRepositoryService extends BaseMeetingRelatedRepository { - if (poll.config.METHOD_NAME === payload.method) { + if (poll.state !== PollState.Created) { + const methodConfig: { + display_chart?: string; + onehundred_percent_base?: string; + required_majority?: string; + } = payload.method_config || {}; + return this.voteApi.update(poll.id, { + title: payload.title, + method_config: { + display_chart: methodConfig.display_chart, + onehundred_percent_base: methodConfig.onehundred_percent_base, + required_majority: methodConfig.required_majority + } + }); + } else if (poll.config.method === payload.method) { delete payload[`method`]; } diff --git a/client/src/app/site/modules/site-wrapper/services/voting-banner.service.ts b/client/src/app/site/modules/site-wrapper/services/voting-banner.service.ts index 812546db44f..425a70ff8d4 100644 --- a/client/src/app/site/modules/site-wrapper/services/voting-banner.service.ts +++ b/client/src/app/site/modules/site-wrapper/services/voting-banner.service.ts @@ -54,6 +54,10 @@ export class VotingBannerService { private async updateVotablePollSubscription(polls: ViewPoll[]): Promise { this.pollsToVoteSubscription?.unsubscribe(); + if (!polls.length) { + this.updateBanner(polls, {}); + return; + } this.pollsToVoteSubscription = this.sendVotesService.subscribeVoted(...polls).subscribe(voted => { this.updateBanner(polls, voted); diff --git a/client/src/app/site/pages/meetings/modules/poll/components/chart/chart.component.html b/client/src/app/site/pages/meetings/modules/poll/components/chart/chart.component.html index acb34997f16..cf8b9864d30 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/chart/chart.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/chart/chart.component.html @@ -1,9 +1,16 @@ -@if (isReadyToShow) { - -} +
+ @if (isReadyToShow) { + + @if (majority !== undefined) { +
+ {{ majority ? 'check' : 'close' }} +
+ } + } +
diff --git a/client/src/app/site/pages/meetings/modules/poll/components/chart/chart.component.scss b/client/src/app/site/pages/meetings/modules/poll/components/chart/chart.component.scss index 6139970a50d..42803a82bf0 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/chart/chart.component.scss +++ b/client/src/app/site/pages/meetings/modules/poll/components/chart/chart.component.scss @@ -1,6 +1,23 @@ -.charts-wrapper { +.chart-wrapper { position: relative; - display: block; - height: 100%; - margin: auto; +} + +.chart-icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + & mat-icon { + font-size: 80px; + width: 80px; + min-width: 80px; + height: 80px; + margin-top: 0 !important; + } + &.color-green { + color: var(--theme-yes); + } + &.color-red { + color: var(--theme-no); + } } diff --git a/client/src/app/site/pages/meetings/modules/poll/components/chart/chart.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/chart/chart.component.ts index 2939a775d29..f7d915c99ab 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/chart/chart.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/chart/chart.component.ts @@ -1,4 +1,5 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { MatIcon } from '@angular/material/icon'; import { ChartjsComponent } from '@coreui/angular-chartjs'; import { ChartData as NgChartData, ChartOptions, ChartType } from 'chart.js'; @@ -25,7 +26,7 @@ type Label = SingleLineLabel | MultiLineLabel; templateUrl: `./chart.component.html`, styleUrls: [`./chart.component.scss`], changeDetection: ChangeDetectionStrategy.Eager, - imports: [ChartjsComponent] + imports: [ChartjsComponent, MatIcon] }) export class ChartComponent { /** @@ -60,6 +61,9 @@ export class ChartComponent { public colors: { backgroundColor?: string[]; hoverBackgroundColor?: string[] }[]; + @Input() + public majority: boolean | undefined; + /** * The general data for the chart. * This is only needed for `type == 'bar' || 'line'` diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-edit-result/poll-edit-result.component.html b/client/src/app/site/pages/meetings/modules/poll/components/poll-edit-result/poll-edit-result.component.html index 10f4d1aea4f..12c5e575b6c 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-edit-result/poll-edit-result.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-edit-result/poll-edit-result.component.html @@ -30,19 +30,21 @@

{{ option.title }}

- - {{ 'majority' | translate }} - +
+ + {{ 'majority' | translate }} + - @if (resultForm.options[i].majority_value().value() === key) { - - } + @if (resultForm.options[i].majority_value().value() === key) { + + } +
diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-edit-result/poll-edit-result.component.scss b/client/src/app/site/pages/meetings/modules/poll/components/poll-edit-result/poll-edit-result.component.scss index e7442a38296..5a1767366fa 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-edit-result/poll-edit-result.component.scss +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-edit-result/poll-edit-result.component.scss @@ -5,3 +5,12 @@ .option-amount { min-width: 50%; } + +.option { + display: flex; +} + +.majority-container { + display: flex; + align-items: center; +} diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-form-approval/poll-form-approval.component.html b/client/src/app/site/pages/meetings/modules/poll/components/poll-form-approval/poll-form-approval.component.html index ab8de93af62..52a0a81f858 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-form-approval/poll-form-approval.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-form-approval/poll-form-approval.component.html @@ -25,6 +25,34 @@ } -
+ + + {{ 'Required majority' | translate }} + + + {{ 'no_majority' | translateKey: 'poll_required_majority' }} + + + {{ 'two_third_majority' | translateKey: 'poll_required_majority' }} + + + {{ 'absolute_majority' | translateKey: 'poll_required_majority' }} + + + @if (form.get('required_majority').getRawValue() === 'absolute_majority') { + + } @else if (form.get('required_majority').getRawValue() === 'two_third_majority') { + + } + diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-form-approval/poll-form-approval.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-form-approval/poll-form-approval.component.ts index caf8c05e737..059fa645674 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-form-approval/poll-form-approval.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-form-approval/poll-form-approval.component.ts @@ -5,6 +5,7 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; import { ApprovalOnehundredPercentBase } from '@app/domain/models/poll/poll-config-approval'; import { ViewPoll } from '@app/site/pages/meetings/pages/polls/view-models'; +import { TranslateKeyPipe } from '@app/ui/pipes/translate-key/translate-key.pipe'; import { _, TranslatePipe } from '@ngx-translate/core'; import { PollFormBaseComponent } from '../poll-config-form-base.component'; @@ -16,7 +17,14 @@ export interface PollFormApproval { @Component({ selector: 'os-poll-form-approval', - imports: [ReactiveFormsModule, MatFormFieldModule, MatSelectModule, MatCheckboxModule, TranslatePipe], + imports: [ + ReactiveFormsModule, + MatFormFieldModule, + MatSelectModule, + MatCheckboxModule, + TranslatePipe, + TranslateKeyPipe + ], templateUrl: './poll-form-approval.component.html', styleUrl: './poll-form-approval.component.scss', changeDetection: ChangeDetectionStrategy.OnPush @@ -37,7 +45,8 @@ export class PollFormApprovalComponent extends PollFormBaseComponent { protected initForm(): void { this.form = this.fb.group({ onehundred_percent_base: [`valid`], - allow_abstain: [false] + allow_abstain: [false], + required_majority: [`no_majority`] }); } @@ -46,6 +55,7 @@ export class PollFormApprovalComponent extends PollFormBaseComponent { if (data.config?.allow_abstain !== undefined) patch[`allow_abstain`] = data.config.allow_abstain; if (data.config?.onehundred_percent_base !== undefined) patch[`onehundred_percent_base`] = data.config.onehundred_percent_base; + if (data.config?.required_majority !== undefined) patch[`required_majority`] = data.config.required_majority; return patch; } diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-form-rating-approval/poll-form-rating-approval.component.html b/client/src/app/site/pages/meetings/modules/poll/components/poll-form-rating-approval/poll-form-rating-approval.component.html index 82fb85459d2..4c936830562 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-form-rating-approval/poll-form-rating-approval.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-form-rating-approval/poll-form-rating-approval.component.html @@ -63,7 +63,35 @@ } -
+ + {{ 'Required majority' | translate }} + + + {{ 'no_majority' | translateKey: 'poll_required_majority' }} + + + {{ 'two_third_majority' | translateKey: 'poll_required_majority' }} + + + {{ 'absolute_majority' | translateKey: 'poll_required_majority' }} + + + @if (form.get('required_majority').getRawValue() === 'absolute_majority') { + + } @else if (form.get('required_majority').getRawValue() === 'two_third_majority') { + + } + diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-form-rating-approval/poll-form-rating-approval.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-form-rating-approval/poll-form-rating-approval.component.ts index 3f0886c6e96..63d4fa010fa 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-form-rating-approval/poll-form-rating-approval.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-form-rating-approval/poll-form-rating-approval.component.ts @@ -1,4 +1,5 @@ import { ChangeDetectionStrategy, Component, effect, inject, input } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { AbstractControl, ReactiveFormsModule, ValidationErrors, ValidatorFn, Validators } from '@angular/forms'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -8,6 +9,7 @@ import { RatingApprovalOnehundredPercentBase } from '@app/domain/models/poll/pol import { RatingScoreOnehundredPercentBase } from '@app/domain/models/poll/poll-config-rating-score'; import { ViewPoll } from '@app/site/pages/meetings/pages/polls/view-models'; import { MeetingSettingsService } from '@app/site/pages/meetings/services/meeting-settings.service'; +import { TranslateKeyPipe } from '@app/ui/pipes/translate-key/translate-key.pipe'; import { _, TranslatePipe } from '@ngx-translate/core'; import { PollFormBaseComponent } from '../poll-config-form-base.component'; @@ -28,7 +30,8 @@ export interface PollFormRatingApproval { MatSelectModule, MatCheckboxModule, MatInputModule, - TranslatePipe + TranslatePipe, + TranslateKeyPipe ], templateUrl: './poll-form-rating-approval.component.html', styleUrls: [`../poll-form/poll-form.component.scss`, `./poll-form-rating-approval.component.scss`], @@ -58,10 +61,18 @@ export class PollFormRatingApprovalComponent extends PollFormBaseComponent { allow_abstain: [false], max_yes_amount: [1, [Validators.required, Validators.min(1)]], max_options_amount: [1, [Validators.required, Validators.min(1)]], - min_options_amount: [1, [Validators.required, Validators.min(0), this.minOptionsAmountValidator()]] + min_options_amount: [1, [Validators.required, Validators.min(0), this.minOptionsAmountValidator()]], + required_majority: [`no_majority`] }); effect(this.onOptionAmountUpdate.bind(this)); + + this.form + .get(`max_options_amount`) + .valueChanges.pipe(takeUntilDestroyed()) + .subscribe(() => { + this.form.get(`min_options_amount`).updateValueAndValidity({ emitEvent: false }); + }); } protected getPatchedFormData(data: Partial): Record { @@ -71,7 +82,8 @@ export class PollFormRatingApprovalComponent extends PollFormBaseComponent { `allow_abstain`, `max_yes_amount`, `max_options_amount`, - `min_options_amount` + `min_options_amount`, + `required_majority` ]) { if (data && data[field] !== undefined) patch[field] = data[field]; else if (data && data.config[field] !== undefined) patch[field] = data.config[field]; diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-form-rating-score/poll-form-rating-score.component.html b/client/src/app/site/pages/meetings/modules/poll/components/poll-form-rating-score/poll-form-rating-score.component.html index 9cc129eb795..d0290936d03 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-form-rating-score/poll-form-rating-score.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-form-rating-score/poll-form-rating-score.component.html @@ -6,6 +6,9 @@ {{ 'Min amount of votes' | translate }} + @if (form.get('min_options_amount').hasError('minGreaterThanMax')) { + {{ 'Amount must not exceed max amount of votes' | translate }} + } } @else {
@@ -68,7 +71,35 @@ } -
+ + {{ 'Required majority' | translate }} + + + {{ 'no_majority' | translateKey: 'poll_required_majority' }} + + + {{ 'two_third_majority' | translateKey: 'poll_required_majority' }} + + + {{ 'absolute_majority' | translateKey: 'poll_required_majority' }} + + + @if (form.get('required_majority').getRawValue() === 'absolute_majority') { + + } @else if (form.get('required_majority').getRawValue() === 'two_third_majority') { + + } + diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-form-rating-score/poll-form-rating-score.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-form-rating-score/poll-form-rating-score.component.ts index ce9874c6c75..cffe8b5f4e5 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-form-rating-score/poll-form-rating-score.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-form-rating-score/poll-form-rating-score.component.ts @@ -1,4 +1,5 @@ import { ChangeDetectionStrategy, Component, effect, input } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { AbstractControl, ReactiveFormsModule, ValidationErrors, ValidatorFn, Validators } from '@angular/forms'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -6,6 +7,7 @@ import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { RatingScoreOnehundredPercentBase } from '@app/domain/models/poll/poll-config-rating-score'; import { ViewPoll } from '@app/site/pages/meetings/pages/polls/view-models'; +import { TranslateKeyPipe } from '@app/ui/pipes/translate-key/translate-key.pipe'; import { _, TranslatePipe } from '@ngx-translate/core'; import { PollFormBaseComponent } from '../poll-config-form-base.component'; @@ -27,7 +29,8 @@ export interface PollFormRatingScore { MatSelectModule, MatCheckboxModule, MatInputModule, - TranslatePipe + TranslatePipe, + TranslateKeyPipe ], templateUrl: './poll-form-rating-score.component.html', styleUrls: [`../poll-form/poll-form.component.scss`, `./poll-form-rating-score.component.scss`], @@ -64,10 +67,25 @@ export class PollFormRatingScoreComponent extends PollFormBaseComponent { [Validators.required, Validators.min(1), this.minOptionsAmountValidator(`max_options_amount`)] ], max_vote_sum: [1, [Validators.required, Validators.min(1)]], - min_vote_sum: [1, [Validators.required, Validators.min(1), this.minOptionsAmountValidator(`max_vote_sum`)]] + min_vote_sum: [1, [Validators.required, Validators.min(1), this.minOptionsAmountValidator(`max_vote_sum`)]], + required_majority: [`no_majority`] }); effect(this.onOptionAmountUpdate.bind(this)); + + this.form + .get(`max_options_amount`) + .valueChanges.pipe(takeUntilDestroyed()) + .subscribe(() => { + this.form.get(`min_options_amount`).updateValueAndValidity({ emitEvent: false }); + }); + + this.form + .get(`max_vote_sum`) + .valueChanges.pipe(takeUntilDestroyed()) + .subscribe(() => { + this.form.get(`min_vote_sum`).updateValueAndValidity({ emitEvent: false }); + }); } protected getPatchedFormData(data: Partial): Record { @@ -78,7 +96,8 @@ export class PollFormRatingScoreComponent extends PollFormBaseComponent { `max_options_amount`, `min_options_amount`, `max_vote_sum`, - `min_vote_sum` + `min_vote_sum`, + `required_majority` ]) { if (data && data[field] !== undefined) patch[field] = data[field]; else if (data && data.config[field] !== undefined) patch[field] = data.config[field]; diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-form-selection/poll-form-selection.component.html b/client/src/app/site/pages/meetings/modules/poll/components/poll-form-selection/poll-form-selection.component.html index b59ae8e9646..288af8899a9 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-form-selection/poll-form-selection.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-form-selection/poll-form-selection.component.html @@ -18,6 +18,14 @@ {{ 'Min amount of votes' | translate }} + @if (form.get('min_options_amount').hasError('min')) { + {{ 'Amount needs to be at least 1' | translate }} + } + @if (form.get('min_options_amount').hasError('minGreaterThanMax')) { + + {{ 'Amount must not exceed amount of max amount of votes' | translate }} + + } } @else {
@@ -69,6 +77,36 @@ + + {{ 'Required majority' | translate }} + + + {{ 'no_majority' | translateKey: 'poll_required_majority' }} + + + {{ 'two_third_majority' | translateKey: 'poll_required_majority' }} + + + {{ 'absolute_majority' | translateKey: 'poll_required_majority' }} + + + @if (form.get('required_majority').getRawValue() === 'absolute_majority') { + + } @else if (form.get('required_majority').getRawValue() === 'two_third_majority') { + + } + + {{ 'Display result as' | translate }} diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-form-selection/poll-form-selection.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-form-selection/poll-form-selection.component.ts index fa68309469a..30fc3e19064 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-form-selection/poll-form-selection.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-form-selection/poll-form-selection.component.ts @@ -1,11 +1,13 @@ import { AsyncPipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, effect, input } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { AbstractControl, ReactiveFormsModule, ValidationErrors, ValidatorFn, Validators } from '@angular/forms'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { SelectionOnehundredPercentBase } from '@app/domain/models/poll/poll-config-selection'; import { ViewPoll } from '@app/site/pages/meetings/pages/polls/view-models'; +import { TranslateKeyPipe } from '@app/ui/pipes/translate-key/translate-key.pipe'; import { _, TranslatePipe } from '@ngx-translate/core'; import { PollFormBaseComponent } from '../poll-config-form-base.component'; @@ -21,7 +23,15 @@ export interface PollFormSelection { @Component({ selector: 'os-poll-form-selection', - imports: [ReactiveFormsModule, MatCheckboxModule, MatInputModule, MatSelectModule, TranslatePipe, AsyncPipe], + imports: [ + ReactiveFormsModule, + MatCheckboxModule, + MatInputModule, + MatSelectModule, + TranslatePipe, + TranslateKeyPipe, + AsyncPipe + ], templateUrl: './poll-form-selection.component.html', styleUrls: [`../poll-form/poll-form.component.scss`, './poll-form-selection.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush @@ -54,9 +64,16 @@ export class PollFormSelectionComponent extends PollFormBaseComponent { allow_general_abstain: [false], max_options_amount: [1, [Validators.required, Validators.min(1)]], min_options_amount: [1, [Validators.required, Validators.min(1), this.minOptionsAmountValidator()]], - display_chart: [`table`] + display_chart: [`table`], + required_majority: [`no_majority`] }); + this.form + .get(`max_options_amount`) + .valueChanges.pipe(takeUntilDestroyed()) + .subscribe(() => { + this.form.get(`min_options_amount`).updateValueAndValidity({ emitEvent: false }); + }); effect(this.onOptionAmountUpdate.bind(this)); } @@ -68,7 +85,8 @@ export class PollFormSelectionComponent extends PollFormBaseComponent { `allow_nota`, `max_options_amount`, `min_options_amount`, - `display_chart` + `display_chart`, + `required_majority` ]) { if (data && data[field] !== undefined) patch[field] = data[field]; else if (data && data.config[field] !== undefined) patch[field] = data.config[field]; diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.ts index adf5b4b9415..7f69b2744bb 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.ts @@ -136,14 +136,6 @@ export class PollFormComponent extends BaseComponent { return null; }); - public methodConfig = computed(() => { - if (this.methodForm()) { - return this.methodForm().getSerialzedForm(); - } - - return null; - }); - public isOpenVotingSelected = computed(() => { return this.form.visibility().value() === PollVisibility.Open || false; }); @@ -176,6 +168,14 @@ export class PollFormComponent extends BaseComponent { effect(this.changeMethod.bind(this)); } + public methodConfig(): Record | null { + if (this.methodForm()) { + return this.methodForm().getSerialzedForm(); + } + + return null; + } + public getValues(): Partial<{ [place in keyof ViewPoll]: any }> { return { ...this.data, ...this.serializeForm() }; } @@ -227,11 +227,25 @@ export class PollFormComponent extends BaseComponent { preselection += data.config.strike_out ? `.no` : `.yes`; } - this.form['method_preselection']().value.set(preselection); + this.updateMethodPreselectionDefault(preselection); } } } + private updateMethodPreselectionDefault(preselection: string): void { + let needsUpdate = this.form['method_preselection']().value() === ''; + const parts = this.form['method_preselection']().value().split(`.`); + if (this.optionAmount() <= 1 && this.form.options().value().length <= 1) { + needsUpdate = needsUpdate || parts[0] !== `approval`; + } else if (this.optionType() === 'text') { + needsUpdate = needsUpdate || (parts[0] !== `selection` && parts[0] !== `rating_score`); + } + + if (needsUpdate) { + this.form['method_preselection']().value.set(preselection); + } + } + private updateConfigData(): void { const configForm = this.methodForm(); if (!configForm) { diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-meta/poll-meta.component.html b/client/src/app/site/pages/meetings/modules/poll/components/poll-meta/poll-meta.component.html index ea25270c2a9..8da3755793d 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-meta/poll-meta.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-meta/poll-meta.component.html @@ -5,7 +5,7 @@ @if (isListPoll()) { @let numOptions = poll().options?.length ?? 0; @if (numOptions === 1) { - {{ 'Nominate' | translate }} + {{ 'Candidate' | translate }} } @else { {{ 'Nomination list' | translate }} ({{ numOptions }} {{ 'Candidates' | translate }}): @@ -69,9 +69,17 @@
{{ 'Voting method' | translate }}: @if (config().strike_out === true) { - {{ 'Disapproval voting' | translate }} + @if (isPersonPoll()) { + {{ 'No per candidate' | translate }} + } @else { + {{ 'No per option' | translate }} + } } @else if (config().strike_out === false) { - {{ 'Approval voting' | translate }} + @if (isPersonPoll()) { + {{ 'Yes per candidate' | translate }} + } @else { + {{ 'Yes per option' | translate }} + } } @else if (config().allow_abstain === true) { {{ 'Yes/No/Abstain' | translate }} } @else if (config().allow_abstain === false) { @@ -148,4 +156,11 @@ {{ config().onehundred_percent_base | translateKey: 'poll_percent_base' }}
} + + @if (config().required_majority !== undefined && config().required_majority !== 'no_majority') { +
+ {{ 'Required majority' | translate }}: + {{ config().required_majority | translateKey: 'poll_required_majority' }} +
+ } diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-progress/poll-progress.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-progress/poll-progress.component.ts index 6d7e066fea8..37f8a405f1d 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-progress/poll-progress.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-progress/poll-progress.component.ts @@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, computed, effect, inject, input, On import { rxResource, takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { Permission } from '@app/domain/definitions/permission'; +import { viewModelListEqual } from '@app/infrastructure/utils'; import { ViewPoll } from '@app/site/pages/meetings/pages/polls/view-models'; import { AutoupdateService, ModelSubscription } from '@app/site/services/autoupdate'; import { ModelRequestBuilderService } from '@app/site/services/model-request-builder'; @@ -9,7 +10,7 @@ import { OperatorService } from '@app/site/services/operator.service'; import { UserControllerService } from '@app/site/services/user-controller.service'; import { BaseUiComponent } from '@app/ui/base/base-ui-component'; import { TranslateModule } from '@ngx-translate/core'; -import { map } from 'rxjs'; +import { distinctUntilChanged, map } from 'rxjs'; import { getParticipantVoteInfoSubscriptionConfig } from '../../../../pages/participants/participants.subscription'; import { ActiveMeetingService } from '../../../../services/active-meeting.service'; @@ -25,10 +26,11 @@ export class PollProgressComponent extends BaseUiComponent implements OnDestroy public poll = input.required(); public votescast = computed(() => { - return Object.keys(this.poll().ballot_user_ids ?? {}).length; + return (this.poll().ballot_user_ids ?? []).length; }); public canSeeProgressBar = signal(false); + public entitledGroupIds = signal([]); public valueInPercent = computed(() => { return (this.votescast() / this.max()) * 100; @@ -41,10 +43,11 @@ export class PollProgressComponent extends BaseUiComponent implements OnDestroy private modelRequestBuilder = inject(ModelRequestBuilderService); public maxResource = rxResource({ - params: () => ({ entitledGroupIds: this.poll().entitled_group_ids }), + params: () => ({ entitledGroupIds: this.entitledGroupIds() }), stream: ({ params }) => this.userRepo.getViewModelListObservable().pipe( + distinctUntilChanged(viewModelListEqual), map(users => { /** * Filter the users who would be able to vote: @@ -87,6 +90,13 @@ export class PollProgressComponent extends BaseUiComponent implements OnDestroy } }); + effect(() => { + const entitledGroups = this.poll().entitled_group_ids; + if (JSON.stringify(entitledGroups) !== JSON.stringify(this.entitledGroupIds())) { + this.entitledGroupIds.set(entitledGroups); + } + }); + this.operator.userObservable.pipe(takeUntilDestroyed()).subscribe(() => { this.canSeeProgressBar.set(this.operator.hasPerms(Permission.pollCanSeeProgress)); }); diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-result-approval/poll-result-approval.component.html b/client/src/app/site/pages/meetings/modules/poll/components/poll-result-approval/poll-result-approval.component.html index 54e4d41cd2a..d16838d7c93 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-result-approval/poll-result-approval.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-result-approval/poll-result-approval.component.html @@ -120,7 +120,7 @@ @if (displayChart()) {
- +
} diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-result-approval/poll-result-approval.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-result-approval/poll-result-approval.component.ts index e7ea88bc747..aceb8952130 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-result-approval/poll-result-approval.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-result-approval/poll-result-approval.component.ts @@ -1,6 +1,6 @@ import { NgTemplateOutlet } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core'; -import { VOTE_MAJORITY } from '@app/domain/models/poll'; +import { PollRequiredMajority, VOTE_MAJORITY } from '@app/domain/models/poll'; import { ApprovalOnehundredPercentBase } from '@app/domain/models/poll/poll-config-approval'; import { ThemeService } from '@app/site/services/theme.service'; import { IconContainerComponent } from '@app/ui/modules/icon-container'; @@ -113,7 +113,9 @@ export class PollResultApprovalComponent extends PollResultBaseComponent k !== `abstain` || this.config().onehundred_percent_base !== `yes_no`) + .filter( + k => (k !== `abstain` || this.config().onehundred_percent_base !== `yes_no`) && (+results[k] || 0) > 0 + ) .map(key => { return { data: [+results[key] || 0], @@ -184,12 +186,29 @@ export class PollResultApprovalComponent extends PollResultBaseComponent(() => { - // TODO: Implement if available - return null; + if (this.config().onehundred_percent_base !== `entitled`) { + return null; + } + + return this.poll().entitled_user_ids?.length; }); public presentEntitledUsers = computed(() => { // TODO: Implement if available return null; }); + + public majorityReached = computed(() => { + if (!this.config().onehundredPercentBaseNum) { + return undefined; + } + + if (this.config().required_majority === PollRequiredMajority.AbsoluteMajority) { + return +this.results().yes > this.config().onehundredPercentBaseNum / 2; + } else if (this.config().required_majority === PollRequiredMajority.TwoThirdMajority) { + return +this.results().yes >= (this.config().onehundredPercentBaseNum * 2) / 3; + } + + return undefined; + }); } diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-result-rating-approval/poll-result-rating-approval.component.html b/client/src/app/site/pages/meetings/modules/poll/components/poll-result-rating-approval/poll-result-rating-approval.component.html index 303e91a0bc1..b0a0886ced8 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-result-rating-approval/poll-result-rating-approval.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-result-rating-approval/poll-result-rating-approval.component.html @@ -6,12 +6,13 @@ + @if (config().allow_abstain) { } - + {{ 'Yes' | translate }} {{ 'No' | translate }} @if (config().allow_abstain) { @@ -44,6 +45,11 @@ {{ i + 1 }}. + + @if (row.majority) { + check + } + + + } diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-result-rating-approval/poll-result-rating-approval.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-result-rating-approval/poll-result-rating-approval.component.ts index 4a6a088cbfb..aadddd7cfad 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-result-rating-approval/poll-result-rating-approval.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-result-rating-approval/poll-result-rating-approval.component.ts @@ -1,5 +1,7 @@ import { NgTemplateOutlet } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed } from '@angular/core'; +import { MatIconModule } from '@angular/material/icon'; +import { PollRequiredMajority } from '@app/domain/models/poll'; import { RatingApprovalOnehundredPercentBase } from '@app/domain/models/poll/poll-config-rating-approval'; import { TranslatePipe } from '@ngx-translate/core'; import Big from 'big.js'; @@ -20,11 +22,12 @@ interface ResultRow { no_percent: number | null; abstain: number; abstain_percent: number | null; + majority: boolean; } @Component({ selector: 'os-poll-result-rating-approval', - imports: [PollVoteOptionComponent, PollParseNumberPipe, TranslatePipe, NgTemplateOutlet], + imports: [PollVoteOptionComponent, PollParseNumberPipe, TranslatePipe, NgTemplateOutlet, MatIconModule], templateUrl: './poll-result-rating-approval.component.html', styleUrl: './poll-result-rating-approval.component.scss', changeDetection: ChangeDetectionStrategy.OnPush @@ -46,6 +49,12 @@ export class PollResultRatingApprovalComponent extends PollResultBaseComponent< const rows: Results = []; for (const option of this.options()) { const onehundredBase = this.config().getOptionOnehundredPercentBaseNum(option); + let majority = false; + if (this.config().required_majority === PollRequiredMajority.AbsoluteMajority) { + majority = (+results[option.id]?.yes || 0) > onehundredBase / 2; + } else if (this.config().required_majority === PollRequiredMajority.TwoThirdMajority) { + majority = (+results[option.id]?.yes || 0) >= (onehundredBase * 2) / 3; + } rows.push({ option, yes: +results[option.id]?.yes || 0, @@ -68,7 +77,8 @@ export class PollResultRatingApprovalComponent extends PollResultBaseComponent< .div(onehundredBase) .mul(100) .toNumber() - : onehundredBase + : onehundredBase, + majority }); } @@ -120,12 +130,28 @@ export class PollResultRatingApprovalComponent extends PollResultBaseComponent< }); public entitledUsers = computed(() => { - // TODO: Implement if available - return null; + if (this.config().onehundred_percent_base !== `entitled`) { + return null; + } + + return this.poll().entitled_user_ids?.length; }); public presentEntitledUsers = computed(() => { // TODO: Implement if available return null; }); + + public globalRequiredMajority = computed(() => { + const baseAmount = this.config().onehundredPercentBaseNum; + if (baseAmount !== null) { + if (this.config().required_majority === PollRequiredMajority.AbsoluteMajority) { + return Math.ceil(baseAmount / 2 + 1); + } else if (this.config().required_majority === PollRequiredMajority.TwoThirdMajority) { + return Math.ceil((baseAmount * 2) / 3); + } + } + + return null; + }); } diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-result-selection/poll-result-selection.component.html b/client/src/app/site/pages/meetings/modules/poll/components/poll-result-selection/poll-result-selection.component.html index 763e697a861..e0ec4376b36 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-result-selection/poll-result-selection.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-result-selection/poll-result-selection.component.html @@ -5,8 +5,10 @@ + + {{ 'Votes' | translate }} @@ -15,6 +17,7 @@ let-amount="amount" let-color="color" let-firstSummary="firstSummary" + let-majority="majority" let-option="option" let-percent="percent" let-votingOption="votingOption" @@ -41,6 +44,12 @@ } + + @if (majority) { + check + } + + @if (percent !== null) { {{ percent | pollParseNumber }}% @@ -146,6 +155,17 @@ }" /> } + + @if (requiredMajority() !== null) { + + } diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-result-selection/poll-result-selection.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-result-selection/poll-result-selection.component.ts index b0d2b0c0493..5dc3eec650d 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-result-selection/poll-result-selection.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-result-selection/poll-result-selection.component.ts @@ -1,6 +1,7 @@ import { NgTemplateOutlet } from '@angular/common'; import { Component, computed, inject, signal } from '@angular/core'; -import { VOTE_MAJORITY } from '@app/domain/models/poll'; +import { MatIconModule } from '@angular/material/icon'; +import { PollRequiredMajority, VOTE_MAJORITY } from '@app/domain/models/poll'; import { ThemeService } from '@app/site/services/theme.service'; import { IconContainerComponent } from '@app/ui/modules/icon-container'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; @@ -20,6 +21,7 @@ interface ResultRow { key: string; votingOption: string; color: string; + majority: boolean; amount: number; percent: number | null; option?: ViewPollOption; @@ -52,7 +54,8 @@ const PollChartBarThickness = 20; PollParseNumberPipe, ChartComponent, TranslatePipe, - NgTemplateOutlet + NgTemplateOutlet, + MatIconModule ], templateUrl: './poll-result-selection.component.html', styleUrl: './poll-result-selection.component.scss' @@ -80,12 +83,14 @@ export class PollResultSelectionComponent extends PollResultBaseComponent< for (const i in this.options()) { const option = this.options()[i]; const optionText = option.getOptionTitle()?.title ?? this.translate.instant(UnknownUserLabel); + const amount = +results[option.id] || 0; rows.push({ key: option.fqid, option: option, votingOption: optionText, color: showPieChart ? colors[i] : null, - amount: +results[option.id] || 0, + amount, + majority: !this.config().strike_out && amount >= this.requiredMajority(), percent: showPercent ? Big(results[option.id] || 0) .div(this.config().onehundredPercentBaseNum) @@ -117,17 +122,6 @@ export class PollResultSelectionComponent extends PollResultBaseComponent< .filter(option => option.data[0]); }); - private generateChartColors(amount: number): string[] { - let colors = Array.from(pollChartColors.values()); - while (colors.length < amount) { - colors = colors.concat(Array.from(pollChartColors.values())); - } - while (colors.length > amount) { - colors.pop(); - } - return colors; - } - public displayChart = computed(() => { const chartData = this.chartData(); if (chartData.some(e => e.data[0] < 0)) { @@ -186,8 +180,11 @@ export class PollResultSelectionComponent extends PollResultBaseComponent< }); public entitledUsers = computed(() => { - // TODO: Implement if available - return null; + if (this.config().onehundred_percent_base !== `entitled`) { + return null; + } + + return this.poll().entitled_user_ids?.length; }); public presentEntitledUsers = computed(() => { @@ -195,6 +192,20 @@ export class PollResultSelectionComponent extends PollResultBaseComponent< return null; }); + public requiredMajority = computed(() => { + if (!this.config().onehundredPercentBaseNum) { + return null; + } + + if (this.config().required_majority === PollRequiredMajority.AbsoluteMajority) { + return Math.ceil(this.config().onehundredPercentBaseNum / 2 + 1); + } else if (this.config().required_majority === PollRequiredMajority.TwoThirdMajority) { + return Math.ceil((this.config().onehundredPercentBaseNum * 2) / 3); + } + + return null; + }); + public generalAbstain = computed(() => { if (this.config().min_options_amount !== 0) { return null; @@ -234,4 +245,15 @@ export class PollResultSelectionComponent extends PollResultBaseComponent< return this.formatResultDecimal((this.nota() / this.config().onehundredPercentBaseNum) * 100); }); + + private generateChartColors(amount: number): string[] { + let colors = Array.from(pollChartColors.values()); + while (colors.length < amount) { + colors = colors.concat(Array.from(pollChartColors.values())); + } + while (colors.length > amount) { + colors.pop(); + } + return colors; + } } diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-rating-approval/poll-vote-rating-approval.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-rating-approval/poll-vote-rating-approval.component.ts index 1a1a71320e8..01712727c1b 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-rating-approval/poll-vote-rating-approval.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-rating-approval/poll-vote-rating-approval.component.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@a import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { YES_KEY } from '@app/domain/models/poll'; +import { MeetingSettingsService } from '@app/site/pages/meetings/services/meeting-settings.service'; import { PromptService } from '@app/ui/modules/prompt-dialog'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; @@ -28,6 +29,7 @@ import { PollVoteOptionComponent } from '../poll-vote-option/poll-vote-option.co export class PollVoteRatingApprovalComponent extends PollVoteBaseComponent { private translate = inject(TranslateService); private promptService = inject(PromptService); + private meetingSettingsService = inject(MeetingSettingsService); public selectedOptions = signal>(new Map()); @@ -40,6 +42,10 @@ export class PollVoteRatingApprovalComponent extends PollVoteBaseComponent(() => { + if (!this.meetingSettingsService.signal(`poll_enable_max_yes_votes`)()) { + return false; + } + const max = this.config()?.max_yes_amount; return max != null && this.yesVotesUsed() >= max; }); diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll/poll.component.html b/client/src/app/site/pages/meetings/modules/poll/components/poll/poll.component.html index d4862077154..dcb764a5708 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll/poll.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll/poll.component.html @@ -4,7 +4,7 @@
- @if (isSameMeeting() && getDetailLink() && navigateToPollDetail()) { + @if (isSameMeeting() && getDetailLink()) { {{ poll().title | translate }} diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll/poll.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll/poll.component.ts index 57c5f4dda2f..2832f5291cb 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll/poll.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll/poll.component.ts @@ -8,6 +8,7 @@ import { MatMenuModule } from '@angular/material/menu'; import { RouterLink } from '@angular/router'; import { PollState, PollVisibility } from '@app/domain/models/poll'; import { infoDialogSettings } from '@app/infrastructure/utils/dialog-settings'; +import { idFromFqid } from '@app/infrastructure/utils/transform-functions'; import { ViewPoll } from '@app/site/pages/meetings/pages/polls/view-models'; import { OperatorService } from '@app/site/services/operator.service'; import { DirectivesModule } from '@app/ui/directives'; @@ -57,7 +58,7 @@ export class PollComponent extends BaseMeetingComponent { public poll = input.required(); public allowEdit = input(false); - public navigateToPollDetail = input(false); + public navigateTo = input<`poll` | `content_object` | null>(null); public dialogOpened = output(); @@ -69,7 +70,19 @@ export class PollComponent extends BaseMeetingComponent { }); public getDetailLink = computed(() => { - return `/${this.poll().meeting_id}/polls/${this.poll().id}`; + if (this.navigateTo() === `poll`) { + return `/${this.poll().meeting_id}/polls/${this.poll().id}`; + } else if (this.navigateTo() === `content_object`) { + if (this.poll().isMotionPoll) { + return `/${this.poll().meeting_id}/motions/${idFromFqid(this.poll().content_object_id)}`; + } else if (this.poll().isAssignmentPoll) { + return `/${this.poll().meeting_id}/assignments/${idFromFqid(this.poll().content_object_id)}`; + } else if (this.poll().isTopicPoll) { + return `/${this.poll().meeting_id}/topics/${idFromFqid(this.poll().content_object_id)}`; + } + } + + return null; }); public pollStateAction: Signal = computed(() => { @@ -156,7 +169,6 @@ export class PollComponent extends BaseMeetingComponent { }); dialogRef.afterClosed().subscribe(result => { - console.log('The dialog was closed'); if (result !== undefined) { this.repo.finalize(this.poll(), result).catch(this.raiseError); } diff --git a/client/src/app/site/pages/meetings/modules/poll/services/entitled-user-filter.service.ts b/client/src/app/site/pages/meetings/modules/poll/services/entitled-user-filter.service.ts index d2c805f62a7..5d4c6d7f673 100644 --- a/client/src/app/site/pages/meetings/modules/poll/services/entitled-user-filter.service.ts +++ b/client/src/app/site/pages/meetings/modules/poll/services/entitled-user-filter.service.ts @@ -15,15 +15,7 @@ export class EntitledUsersListFilterService extends BaseFilterListService { protected getFilterDefinitions(): OsFilter[] { return [ { - property: `present`, - label: _(`Is present`), - options: [ - { label: _(`Is present`), condition: true }, - { label: _(`Is not present`), condition: [false, null] } - ] - }, - { - property: `voted`, + property: `hasVoted`, label: _(`Voted`), options: [ { label: _(`Has voted`), condition: true }, diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll/topic-poll.component.html b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll/topic-poll.component.html index 57a35eaf387..37a4ce22d76 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll/topic-poll.component.html +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll/topic-poll.component.html @@ -2,8 +2,8 @@ diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/services/topic-poll.service.ts b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/services/topic-poll.service.ts index 1258a286e44..69eae86eda5 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/services/topic-poll.service.ts +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/services/topic-poll.service.ts @@ -1,7 +1,5 @@ import { inject, Service } from '@angular/core'; import { PollVisibility } from '@app/domain/models/poll'; -import { SelectionOnehundredPercentBase } from '@app/domain/models/poll/poll-config-selection'; -import { BaseOnehundredPercentBase } from '@app/domain/models/poll/poll-config-types'; import { Topic } from '@app/domain/models/topics/topic'; import { PollService } from '@app/site/pages/meetings/modules/poll/services/poll.service'; import { PollControllerService } from '@app/site/pages/meetings/modules/poll/services/poll-controller.service'; @@ -13,63 +11,45 @@ import { ViewTopic } from '../../../view-models/view-topic'; @Service() export class TopicPollService extends PollService { - private defaultPercentBase: BaseOnehundredPercentBase | SelectionOnehundredPercentBase; - private defaultGroupIds: number[]; - private defaultEnableLiveVote = false; - private defaultPollType: PollVisibility | undefined; - private defaultVotingType: string; - private defaultDisplayChart: string; - private defaultAllowAbstain = false; - private defaultAllowNota = false; - private defaultActiveStrikeOut = false; - private pollRepo = inject(PollControllerService); private meetingPollSettingsService = inject(MeetingPollSettingsService); + private defaultPercentBase = this.meetingPollSettingsService.signal(`topic`, `onehundred_percent_base`); + private defaultGroupIds = this.meetingPollSettingsService.signal(`topic`, `group_ids`); + private defaultEnableLiveVote = this.meetingSettingsService.signal(`poll_default_live_voting_enabled`); + private defaultPollType = this.meetingPollSettingsService.signal(`topic`, `visibility`); + private defaultVotingType = this.meetingSettingsService.signal(`topic_poll_default_method`); + private defaultDisplayChart = this.meetingPollSettingsService.signal(`topic`, `display_chart`); + private defaultAllowAbstain = this.meetingPollSettingsService.signal(`topic`, `allow_abstain`); + private defaultAllowNota = this.meetingPollSettingsService.signal(`topic`, `allow_nota`); + private defaultActiveStrikeOut = this.meetingPollSettingsService.signal(`topic`, `strike_out`); + private defaultRequiredMajority = this.meetingSettingsService.signal(`poll_default_required_majority`); + public constructor() { super(); const pollServiceMapper = inject(PollServiceMapperService); pollServiceMapper.registerService(ViewTopic.COLLECTION, this); - this.meetingPollSettingsService - .get(`topic`, `onehundred_percent_base`) - .subscribe(base => (this.defaultPercentBase = base ?? `valid`)); - this.meetingPollSettingsService.get(`topic`, `group_ids`).subscribe(ids => (this.defaultGroupIds = ids ?? [])); this.meetingPollSettingsService .get(`topic`, `sort_result_by_votes`) .subscribe(sort => (this.sortByVote = sort)); - this.meetingPollSettingsService - .get(`topic`, `allow_abstain`) - .subscribe(bool => (this.defaultAllowAbstain = bool)); - this.meetingPollSettingsService.get(`topic`, `visibility`).subscribe(type => (this.defaultPollType = type)); - this.meetingPollSettingsService.get(`topic`, `allow_nota`).subscribe(bool => (this.defaultAllowNota = bool)); - this.meetingPollSettingsService - .get(`topic`, `strike_out`) - .subscribe(bool => (this.defaultActiveStrikeOut = bool)); - this.meetingPollSettingsService - .get(`topic`, `display_chart`) - .subscribe(chartType => (this.defaultDisplayChart = chartType)); - - this.meetingSettingsService - .get(`poll_default_live_voting_enabled`) - .subscribe(is => (this.defaultEnableLiveVote = is)); - this.meetingSettingsService.get(`topic_poll_default_method`).subscribe(type => (this.defaultVotingType = type)); } public getDefaultPollData(contentObject?: Topic): Partial { const poll: Partial = { - title: this.translate.instant(`Ballot`), - entitled_group_ids: Object.values(this.defaultGroupIds ?? []), - visibility: this.isElectronicVotingEnabled ? this.defaultPollType : PollVisibility.Manually, - live_voting_enabled: this.defaultEnableLiveVote, + title: this.translate.instant(`Poll`), + entitled_group_ids: Object.values(this.defaultGroupIds() ?? []), + visibility: this.isElectronicVotingEnabled ? this.defaultPollType() : PollVisibility.Manually, + live_voting_enabled: this.defaultEnableLiveVote(), config: { - allow_abstain: this.defaultAllowAbstain, - allow_nota: this.defaultAllowNota, - strike_out: this.defaultActiveStrikeOut, - display_chart: this.defaultDisplayChart, - voting_type: this.defaultVotingType, - onehundred_percent_base: this.defaultPercentBase, - method: this.defaultVotingType + allow_abstain: this.defaultAllowAbstain(), + allow_nota: this.defaultAllowNota(), + strike_out: this.defaultActiveStrikeOut(), + display_chart: this.defaultDisplayChart(), + voting_type: this.defaultVotingType(), + onehundred_percent_base: this.defaultPercentBase(), + method: this.defaultVotingType(), + required_majority: this.defaultRequiredMajority() } }; diff --git a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.ts b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.ts index 0e1eb410b96..d0ddd979155 100644 --- a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.ts +++ b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.ts @@ -68,7 +68,7 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent { options.push([{ key: `approval`, title: null }]); } else { for (const option of assignment.candidates) { - options.push({ key: `meeting_user-${option.meeting_user_id}`, title: option.getTitle() }); + options.push({ key: `meeting_user/${option.meeting_user_id}`, title: option.getTitle() }); } } diff --git a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll/assignment-poll.component.html b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll/assignment-poll.component.html index 57a35eaf387..37a4ce22d76 100644 --- a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll/assignment-poll.component.html +++ b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll/assignment-poll.component.html @@ -2,8 +2,8 @@ diff --git a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll/assignment-poll.component.ts b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll/assignment-poll.component.ts index c0fb74838e9..29ebd0afd39 100644 --- a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll/assignment-poll.component.ts +++ b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll/assignment-poll.component.ts @@ -12,6 +12,7 @@ import { PollControllerService } from '@app/site/pages/meetings/modules/poll/ser import { ViewPoll } from '@app/site/pages/meetings/pages/polls/view-models'; import { OperatorService } from '@app/site/services/operator.service'; import { DirectivesModule } from '@app/ui/directives'; +import { filter } from 'rxjs'; @Component({ selector: `os-assignment-poll`, @@ -29,7 +30,7 @@ export class AssignmentPollComponent extends BaseMeetingComponent { public poll = rxResource({ params: () => this.pollId(), - stream: ({ params }) => this.repo.getViewModelObservable(params) + stream: ({ params }) => this.repo.getViewModelObservable(params).pipe(filter(p => !!p)) }); public showPoll = computed(() => { diff --git a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/services/assignment-poll.service.ts b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/services/assignment-poll.service.ts index c2f5c5fd62d..5148daf72d2 100644 --- a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/services/assignment-poll.service.ts +++ b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/services/assignment-poll.service.ts @@ -1,7 +1,6 @@ import { inject, Service } from '@angular/core'; import { Assignment } from '@app/domain/models/assignments/assignment'; import { PollVisibility } from '@app/domain/models/poll'; -import { BaseOnehundredPercentBase } from '@app/domain/models/poll/poll-config-types'; import { PollServiceMapperService } from '@app/site/pages/meetings/modules/poll/services/poll-service-mapper.service'; import { ViewAssignment } from '@app/site/pages/meetings/pages/assignments'; import { ViewPoll } from '@app/site/pages/meetings/pages/polls/view-models'; @@ -20,72 +19,47 @@ export const UnknownUserLabel = _(`Deleted user`); */ @Service() export class AssignmentPollService extends PollService { - private defaultPercentBase: BaseOnehundredPercentBase | undefined; - private defaultPollType: PollVisibility | undefined; - private defaultVotingType: string; - private defaultDisplayChart: string; - private defaultGroupIds: number[] = []; - private defaultAllowAbstain = false; - private defaultAllowNota = false; - private defaultActiveStrikeOut = false; - private defaultEnableLiveVote = false; - private pollRepo = inject(PollControllerService); private meetingPollSettingsService = inject(MeetingPollSettingsService); + private defaultPercentBase = this.meetingPollSettingsService.signal(`topic`, `onehundred_percent_base`); + private defaultGroupIds = this.meetingPollSettingsService.signal(`topic`, `group_ids`); + private defaultEnableLiveVote = this.meetingSettingsService.signal(`poll_default_live_voting_enabled`); + private defaultPollType = this.meetingPollSettingsService.signal(`topic`, `visibility`); + private defaultVotingType = this.meetingSettingsService.signal(`topic_poll_default_method`); + private defaultDisplayChart = this.meetingPollSettingsService.signal(`topic`, `display_chart`); + private defaultAllowAbstain = this.meetingPollSettingsService.signal(`topic`, `allow_abstain`); + private defaultAllowNota = this.meetingPollSettingsService.signal(`topic`, `allow_nota`); + private defaultActiveStrikeOut = this.meetingPollSettingsService.signal(`topic`, `strike_out`); + private defaultRequiredMajority = this.meetingSettingsService.signal(`poll_default_required_majority`); + public constructor() { super(); const pollServiceMapper = inject(PollServiceMapperService); pollServiceMapper.registerService(ViewAssignment.COLLECTION, this); - this.meetingPollSettingsService - .get(`assignment`, `onehundred_percent_base`) - .subscribe(base => (this.defaultPercentBase = base)); - this.meetingPollSettingsService - .get(`assignment`, `group_ids`) - .subscribe(ids => (this.defaultGroupIds = ids ?? [])); this.meetingPollSettingsService .get(`assignment`, `sort_result_by_votes`) .subscribe(sort => (this.sortByVote = sort)); - this.meetingPollSettingsService - .get(`assignment`, `allow_abstain`) - .subscribe(bool => (this.defaultAllowAbstain = bool)); - this.meetingPollSettingsService - .get(`assignment`, `visibility`) - .subscribe(type => (this.defaultPollType = type)); - this.meetingPollSettingsService - .get(`assignment`, `allow_nota`) - .subscribe(bool => (this.defaultAllowNota = bool)); - this.meetingPollSettingsService - .get(`assignment`, `strike_out`) - .subscribe(bool => (this.defaultActiveStrikeOut = bool)); - this.meetingPollSettingsService - .get(`assignment`, `display_chart`) - .subscribe(chartType => (this.defaultDisplayChart = chartType)); this.meetingSettingsService .get(`poll_enable_max_votes_per_option`) .subscribe(enable_max_votes_per_option => (this.enableMaxVotesPerOption = enable_max_votes_per_option)); - this.meetingSettingsService - .get(`poll_default_live_voting_enabled`) - .subscribe(is => (this.defaultEnableLiveVote = is)); - this.meetingSettingsService - .get(`assignment_poll_default_method`) - .subscribe(type => (this.defaultVotingType = type)); } public getDefaultPollData(contentObject?: Assignment): Partial { const poll: Partial = { - title: this.translate.instant(`Ballot`), - entitled_group_ids: Object.values(this.defaultGroupIds ?? []), - visibility: this.isElectronicVotingEnabled ? this.defaultPollType : PollVisibility.Manually, - live_voting_enabled: this.defaultEnableLiveVote, + title: this.translate.instant(`Poll`), + entitled_group_ids: Object.values(this.defaultGroupIds() ?? []), + visibility: this.isElectronicVotingEnabled ? this.defaultPollType() : PollVisibility.Manually, + live_voting_enabled: this.defaultEnableLiveVote(), config: { - allow_abstain: this.defaultAllowAbstain, - allow_nota: this.defaultAllowNota, - strike_out: this.defaultActiveStrikeOut, - onehundred_percent_base: this.defaultPercentBase, - display_chart: this.defaultDisplayChart, - method: this.defaultVotingType + allow_abstain: this.defaultAllowAbstain(), + allow_nota: this.defaultAllowNota(), + strike_out: this.defaultActiveStrikeOut(), + onehundred_percent_base: this.defaultPercentBase(), + display_chart: this.defaultDisplayChart(), + method: this.defaultVotingType(), + required_majority: this.defaultRequiredMajority() } }; diff --git a/client/src/app/site/pages/meetings/pages/autopilot/components/poll-collection/poll-collection.component.html b/client/src/app/site/pages/meetings/pages/autopilot/components/poll-collection/poll-collection.component.html index 17952f4e4d0..f74de1a456b 100644 --- a/client/src/app/site/pages/meetings/pages/autopilot/components/poll-collection/poll-collection.component.html +++ b/client/src/app/site/pages/meetings/pages/autopilot/components/poll-collection/poll-collection.component.html @@ -1,5 +1,5 @@ @if (!disableRunning) { - @for (poll of polls; track identifyPoll($index, poll)) { + @for (poll of polls; track poll.id) { } } @@ -10,7 +10,7 @@ - + diff --git a/client/src/app/site/pages/meetings/pages/autopilot/components/poll-collection/poll-collection.component.ts b/client/src/app/site/pages/meetings/pages/autopilot/components/poll-collection/poll-collection.component.ts index becb8a35a72..2a08890c828 100644 --- a/client/src/app/site/pages/meetings/pages/autopilot/components/poll-collection/poll-collection.component.ts +++ b/client/src/app/site/pages/meetings/pages/autopilot/components/poll-collection/poll-collection.component.ts @@ -105,10 +105,6 @@ export class PollCollectionComponent extends BaseCo super.ngOnDestroy(); } - public identifyPoll(index: number, poll: ViewPoll): number { - return poll.id; - } - public getPollVoteTitle(poll: ViewPoll): string { const contentObject = poll.getContentObject(); const listTitle = contentObject.getListTitle(); diff --git a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll/motion-poll.component.html b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll/motion-poll.component.html index b02791d08fe..8831e28f9ee 100644 --- a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll/motion-poll.component.html +++ b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll/motion-poll.component.html @@ -2,8 +2,8 @@ diff --git a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/services/motion-poll.service/motion-poll.service.ts b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/services/motion-poll.service/motion-poll.service.ts index a009e7cd055..94552638f36 100644 --- a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/services/motion-poll.service/motion-poll.service.ts +++ b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/services/motion-poll.service/motion-poll.service.ts @@ -1,6 +1,5 @@ import { inject, Service } from '@angular/core'; import { Motion } from '@app/domain/models/motions/motion'; -import { BaseOnehundredPercentBase } from '@app/domain/models/poll/poll-config-types'; import { PollVisibility } from '@app/domain/models/poll/poll-constants'; import { PollService } from '@app/site/pages/meetings/modules/poll/services/poll.service/poll.service'; import { ViewPoll } from '@app/site/pages/meetings/pages/polls/view-models'; @@ -13,35 +12,27 @@ import { MotionPollControllerService } from '../motion-poll-controller.service/m */ @Service() export class MotionPollService extends PollService { - public defaultPercentBase!: BaseOnehundredPercentBase; - public defaultPollVisibility!: PollVisibility; - public defaultGroupIds!: number[]; - public defaultAllowAbstain = false; - private repo = inject(MotionPollControllerService); private meetingPollSettingsService = inject(MeetingPollSettingsService); + private defaultPercentBase = this.meetingPollSettingsService.signal(`motion`, `onehundred_percent_base`); + private defaultPollVisibility = this.meetingPollSettingsService.signal(`motion`, `visibility`); + private defaultGroupIds = this.meetingPollSettingsService.signal(`motion`, `group_ids`); + private defaultAllowAbstain = this.meetingPollSettingsService.signal(`motion`, `allow_abstain`); + private defaultRequiredMajority = this.meetingSettingsService.signal(`poll_default_required_majority`); + public constructor() { super(); - this.meetingPollSettingsService - .get(`motion`, `onehundred_percent_base`) - .subscribe(base => (this.defaultPercentBase = base)); - this.meetingPollSettingsService - .get(`motion`, `visibility`) - .subscribe(type => (this.defaultPollVisibility = type as any)); - this.meetingPollSettingsService.get(`motion`, `group_ids`).subscribe(ids => (this.defaultGroupIds = ids ?? [])); - this.meetingPollSettingsService - .get(`motion`, `allow_abstain`) - .subscribe(bool => (this.defaultAllowAbstain = bool)); } public getDefaultPollData(contentObject?: Motion): Partial { const poll: Partial = { - entitled_group_ids: Object.values(this.defaultGroupIds ?? []), - visibility: this.isElectronicVotingEnabled ? this.defaultPollVisibility : PollVisibility.Manually, + entitled_group_ids: Object.values(this.defaultGroupIds() ?? []), + visibility: this.isElectronicVotingEnabled ? this.defaultPollVisibility() : PollVisibility.Manually, config: { - allow_abstain: this.defaultAllowAbstain, - onehundred_percent_base: this.defaultPercentBase + allow_abstain: this.defaultAllowAbstain(), + onehundred_percent_base: this.defaultPercentBase(), + required_majority: this.defaultRequiredMajority() } }; diff --git a/client/src/app/site/pages/meetings/pages/participants/pages/participant-list/components/participant-list/participant-list.component.html b/client/src/app/site/pages/meetings/pages/participants/pages/participant-list/components/participant-list/participant-list.component.html index 439ff408772..6b038ff7e0c 100644 --- a/client/src/app/site/pages/meetings/pages/participants/pages/participant-list/components/participant-list/participant-list.component.html +++ b/client/src/app/site/pages/meetings/pages/participants/pages/participant-list/components/participant-list/participant-list.component.html @@ -160,7 +160,7 @@

Participants

} - @if (user.vote_delegated_to() && voteDelegationEnabled) { + @if (user.vote_delegated_to_ids() && voteDelegationEnabled) {
= toObservable(this.poll).pipe( switchMap(poll => combineLatest([poll.entitled_users$, poll.ballot_users$.pipe(startWith([]))])), diff --git a/client/src/app/site/pages/meetings/pages/polls/components/poll-single-votes/poll-single-votes.component.html b/client/src/app/site/pages/meetings/pages/polls/components/poll-single-votes/poll-single-votes.component.html index 943aad7d08c..e48a4bb8f13 100644 --- a/client/src/app/site/pages/meetings/pages/polls/components/poll-single-votes/poll-single-votes.component.html +++ b/client/src/app/site/pages/meetings/pages/polls/components/poll-single-votes/poll-single-votes.component.html @@ -1,7 +1,7 @@
= toObservable(this.poll).pipe( - switchMap(poll => (poll.state === PollState.Finished ? poll.ballots$ : of([]))), + switchMap(poll => (poll.published ? poll.ballots$ : of([]))), map((ballots: ViewPollBallot[]) => ballots.map(ballot => { const user = ballot.represented_meeting_user?.user; @@ -83,6 +82,8 @@ export class PollSingleVotesComponent extends BaseComponent { : null, user: user, id: user?.id, + groupIds: ballot.groupIds, + structureLevelIds: ballot.structureLevelIds, weight: +ballot.weight !== 1 ? +ballot.weight : undefined, value: this.parseVoteValue(ballot.value), valueRaw: ballot.parsedValue() diff --git a/client/src/app/site/pages/meetings/pages/polls/view-models/base-poll-result.ts b/client/src/app/site/pages/meetings/pages/polls/view-models/base-poll-result.ts index 318e652a065..55fe9a2e7bf 100644 --- a/client/src/app/site/pages/meetings/pages/polls/view-models/base-poll-result.ts +++ b/client/src/app/site/pages/meetings/pages/polls/view-models/base-poll-result.ts @@ -1,4 +1,5 @@ import { djb2hash } from '@app/infrastructure/utils'; +import { isFqid } from '@app/infrastructure/utils/transform-functions'; import { BasePollConfigViewModel } from './base-poll-config-view-model'; @@ -17,18 +18,14 @@ export abstract class BasePollResult djb2hash(o.text) === keyParts[1]); if (option) { input[option.id.toString()] = input[key]; delete input[key]; } - } else { + } else if (isFqid(key)) { const option = options.find(o => o.content_object_id === key); if (option) { input[option.id.toString()] = input[key]; diff --git a/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-rating-approval.ts b/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-rating-approval.ts index 451b26f77ba..d0771ac4e7a 100644 --- a/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-rating-approval.ts +++ b/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-rating-approval.ts @@ -33,7 +33,7 @@ export class ViewPollConfigRatingApproval extends BasePollConfigViewModel< case 'cast': return this.totalVotes; case 'entitled': - return null; + return this.poll.entitled_user_ids?.length; case 'entitled_present': return null; } @@ -62,7 +62,7 @@ export class ViewPollConfigRatingApproval extends BasePollConfigViewModel< case 'cast': return this.poll.ballot_user_ids?.length || 0; case 'entitled': - return null; + return this.poll.entitled_user_ids?.length; case 'entitled_present': return null; } diff --git a/client/src/app/site/pages/meetings/pages/projectors/modules/projector-list/components/projector-list/projector-list.component.html b/client/src/app/site/pages/meetings/pages/projectors/modules/projector-list/components/projector-list/projector-list.component.html index 432fe7cb7c5..649736931d2 100644 --- a/client/src/app/site/pages/meetings/pages/projectors/modules/projector-list/components/projector-list/projector-list.component.html +++ b/client/src/app/site/pages/meetings/pages/projectors/modules/projector-list/components/projector-list/projector-list.component.html @@ -44,7 +44,7 @@

- @for (projector of projectors | async; track $index) { + @for (projector of projectors | async; track projector.id) {
diff --git a/client/src/app/site/pages/meetings/services/meeting-poll-settings.service.ts b/client/src/app/site/pages/meetings/services/meeting-poll-settings.service.ts index 9e59d1249c6..f2b3cc228ff 100644 --- a/client/src/app/site/pages/meetings/services/meeting-poll-settings.service.ts +++ b/client/src/app/site/pages/meetings/services/meeting-poll-settings.service.ts @@ -1,15 +1,22 @@ -import { inject, Injectable } from '@angular/core'; +import { inject, Service, Signal, signal, WritableSignal } from '@angular/core'; import { MeetingPollDefault } from '@app/domain/models/meetings/meeting-poll-default'; -import { BehaviorSubject, combineLatest, Observable, switchMap } from 'rxjs'; +import { BehaviorSubject, combineLatest, filter, Observable, switchMap } from 'rxjs'; import { ActiveMeetingService } from './active-meeting.service'; type MeetingPollSettingCollection = 'motion' | 'topic' | 'assignment'; -@Injectable({ - providedIn: `root` -}) +@Service() export class MeetingPollSettingsService { + /** + * Stores a subject per key. Values are published, if the DataStore gets an update. + */ + private settingSignals: Record>> = { + assignment: {}, + motion: {}, + topic: {} + }; + /** * Stores a subject per key. Values are published, if the DataStore gets an update. */ @@ -27,6 +34,7 @@ export class MeetingPollSettingsService { public constructor() { this.activeMeetingService.meetingObservable .pipe( + filter(m => !!m), switchMap(m => { return combineLatest([m.assignment_poll_config$, m.motion_poll_config$, m.topic_poll_config$]); }) @@ -38,6 +46,12 @@ export class MeetingPollSettingsService { this.settingSubjects[`assignment`][key].next(assignmentPollConfig[key]); } } + + for (const key of Object.keys(this.settingSignals[`assignment`])) { + if (this.settingSignals[`assignment`][key]() !== assignmentPollConfig[key]) { + this.settingSignals[`assignment`][key].set(assignmentPollConfig[key]); + } + } } if (motionPollConfig) { @@ -46,6 +60,12 @@ export class MeetingPollSettingsService { this.settingSubjects[`motion`][key].next(motionPollConfig[key]); } } + + for (const key of Object.keys(this.settingSignals[`motion`])) { + if (this.settingSignals[`motion`][key]() !== motionPollConfig[key]) { + this.settingSignals[`motion`][key].set(motionPollConfig[key]); + } + } } if (topicPollConfig) { @@ -54,6 +74,12 @@ export class MeetingPollSettingsService { this.settingSubjects[`topic`][key].next(topicPollConfig[key]); } } + + for (const key of Object.keys(this.settingSignals[`topic`])) { + if (this.settingSignals[`topic`][key]() !== topicPollConfig[key]) { + this.settingSignals[`topic`][key].set(topicPollConfig[key]); + } + } } }); } @@ -87,4 +113,19 @@ export class MeetingPollSettingsService { } return this.settingSubjects[collection][key] as Observable; } + + /** + * Get a signal for the setting value given by the key. + * + * @param key The setting value to get from. + */ + public signal( + collection: MeetingPollSettingCollection, + key: T + ): Signal { + if (!this.settingSignals[collection][key]) { + this.settingSignals[collection][key] = signal(this.instant(collection, key)); + } + return this.settingSignals[collection][key] as Signal; + } } diff --git a/client/src/app/site/pages/meetings/view-models/view-meeting-user.ts b/client/src/app/site/pages/meetings/view-models/view-meeting-user.ts index 6a1dc166972..7dafd16067c 100644 --- a/client/src/app/site/pages/meetings/view-models/view-meeting-user.ts +++ b/client/src/app/site/pages/meetings/view-models/view-meeting-user.ts @@ -49,7 +49,7 @@ interface IMeetingUserRelations { motion_working_group_speakers: ViewMotionWorkingGroupSpeaker[]; acting_ballots: ViewPollBallotUser[]; represented_ballots: ViewPollBallotUser[]; - vote_delegated_to: ViewMeetingUser; + vote_delegated_to: ViewMeetingUser[]; vote_delegations_from: ViewMeetingUser[]; structure_levels: ViewStructureLevel[]; poll_options: ViewPollOption[]; diff --git a/client/src/app/site/pages/meetings/view-models/view-user.ts b/client/src/app/site/pages/meetings/view-models/view-user.ts index d98da009b93..0b11f061083 100644 --- a/client/src/app/site/pages/meetings/view-models/view-user.ts +++ b/client/src/app/site/pages/meetings/view-models/view-user.ts @@ -208,10 +208,6 @@ export class ViewUser extends BaseViewModel /* implements Searchable */ { return this.vote_delegations_from_ids(meetingId || this.getEnsuredActiveMeetingId())?.length > 0; } - public delegationName(meetingId?: Id): string | undefined { - return this.vote_delegated_to(meetingId || this.getEnsuredActiveMeetingId())?.getFullName(); - } - public speaker_ids(meetingId?: Id): Id[] { return this.getMeetingUser(meetingId)?.speaker_ids; } @@ -236,8 +232,8 @@ export class ViewUser extends BaseViewModel /* implements Searchable */ { return this.getMeetingUser(meetingId)?.chat_message_ids; } - public vote_delegated_to_ids(meetingId?: Id): Id { - return this.getMeetingUser(meetingId)?.vote_delegated_to?.user_id; + public vote_delegated_to_ids(meetingId?: Id): Id[] { + return this.getMeetingUser(meetingId)?.vote_delegated_to_ids; } public vote_delegated_to_meeting_user_ids(meetingId?: Id): Id[] { @@ -311,13 +307,13 @@ export class ViewUser extends BaseViewModel /* implements Searchable */ { } public get isVoteCountable(): boolean { - const delegate = this.vote_delegated_to(this.getEnsuredActiveMeetingId()); + const delegates = this.vote_delegated_to(this.getEnsuredActiveMeetingId()); const present = this.isPresentInMeeting(); if (this.isSelfVotingAllowedDespiteDelegation() && present) { return true; } - if (this.getDelegationSettingEnabled() && delegate) { - return delegate.isPresentInMeeting(); + if (this.getDelegationSettingEnabled() && delegates.length) { + return delegates.some(delegate => delegate.isPresentInMeeting()); } return present; } @@ -355,12 +351,12 @@ export class ViewUser extends BaseViewModel /* implements Searchable */ { return this.vote_delegations_from_ids()?.includes(user.id); } - public vote_delegated_to_meeting_user(meetingId?: number): ViewMeetingUser { + public vote_delegated_to_meeting_user(meetingId?: number): ViewMeetingUser[] { return this.getMeetingUser(meetingId)?.vote_delegated_to; } - public vote_delegated_to(meetingId?: number): ViewUser { - return this.vote_delegated_to_meeting_user(meetingId)?.user; + public vote_delegated_to(meetingId?: number): ViewUser[] { + return this.vote_delegated_to_meeting_user(meetingId)?.map(meeting_user => meeting_user.user) || []; } public vote_delegations_from_meeting_users(meetingId?: number): ViewMeetingUser[] { diff --git a/client/src/assets/styles/poll/result-table.scss b/client/src/assets/styles/poll/result-table.scss index 345b89d9f0c..4fa2ff3bfa2 100644 --- a/client/src/assets/styles/poll/result-table.scss +++ b/client/src/assets/styles/poll/result-table.scss @@ -48,6 +48,12 @@ .result-percent-small { font-size: 14px; } + + .check-icon { + width: 20px; + color: var(--theme-yes); + padding-top: 0.5em; + } } } }