diff --git a/client/src/app/domain/models/meetings/meeting.ts b/client/src/app/domain/models/meetings/meeting.ts index f0686e461d5..861aa8d7a0c 100644 --- a/client/src/app/domain/models/meetings/meeting.ts +++ b/client/src/app/domain/models/meetings/meeting.ts @@ -172,11 +172,14 @@ export class Settings { public topic_poll_config_id: Id; // General poll settings + public poll_enable_max_yes_votes: boolean; + public poll_default_required_majority: `no_majority` | `two_third_majority` | `absolute_majority`; public poll_default_live_voting_enabled: boolean; public poll_enable_max_votes_per_option: boolean; public poll_default_allow_invalid: boolean; public poll_default_allow_vote_split: boolean; public poll_projection_name_order_first: `first_name` | `last_name`; + public poll_projection_max_columns: boolean; // SSO public external_id!: string; @@ -412,11 +415,14 @@ export class Meeting extends BaseModel { `motion_poll_config_id`, `topic_poll_default_method`, `topic_poll_config_id`, + `poll_enable_max_yes_votes`, `poll_enable_max_votes_per_option`, + `poll_default_required_majority`, `poll_default_live_voting_enabled`, `poll_default_allow_invalid`, `poll_default_allow_vote_split`, `poll_projection_name_order_first`, + `poll_projection_max_columns`, `projector_ids`, `all_projection_ids`, `projector_message_ids`, diff --git a/client/src/app/domain/translations/poll.ts b/client/src/app/domain/translations/poll.ts index e169ef1d7d3..db2b8043e7b 100644 --- a/client/src/app/domain/translations/poll.ts +++ b/client/src/app/domain/translations/poll.ts @@ -22,5 +22,8 @@ export default { 'poll_percent_base.cast': _(`All casted ballots`), 'poll_percent_base.entitled': _(`All entitled users`), 'poll_percent_base.entitled_present': _(`Present entitled users`), - 'poll_percent_base.disabled': _(`Disabled (no percents)`) + 'poll_percent_base.disabled': _(`Disabled (no percents)`), + 'poll_required_majority.no_majority': _(`No majority`), + 'poll_required_majority.two_third_majority': _(`Two thirds`), + 'poll_required_majority.absolute_majority': _(`Absolute`) }; 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 2914fe89bf3..3f5422840f3 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 @@ -136,6 +136,13 @@ export class PollRepositoryService extends BaseMeetingRelatedRepository { + return this.voteApi.finalize(poll.id, { + anonymize: actions.indexOf('anonymize') !== -1, + publish: actions.indexOf('publish') !== -1 + }); + } + public async updateOptionForPoll(poll: Poll, update: any): Promise { if (poll.visibility !== PollVisibility.Manually) { throw new Error(`Cannot update an option for an electronic poll!`); diff --git a/client/src/app/site/pages/meetings/modules/poll/base/base-poll-dialog.component.ts b/client/src/app/site/pages/meetings/modules/poll/base/base-poll-dialog.component.ts index be5ec9ecf0d..24b39189e14 100644 --- a/client/src/app/site/pages/meetings/modules/poll/base/base-poll-dialog.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/base/base-poll-dialog.component.ts @@ -1,5 +1,4 @@ -import { Directive, inject, signal, viewChild } from '@angular/core'; -import { rxResource } from '@angular/core/rxjs-interop'; +import { computed, Directive, inject, signal, viewChild } from '@angular/core'; import { UntypedFormBuilder } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Fqid, Id } from '@app/domain/definitions/key-types'; @@ -8,7 +7,6 @@ import { PollVisibility } from '@app/domain/models/poll'; import { PollUpdatePayload } from '@app/gateways/vote-api.service'; import { ViewPoll } from '@app/site/pages/meetings/pages/polls'; import { BaseUiComponent } from '@app/ui/base/base-ui-component'; -import { map } from 'rxjs'; import { PollEditResultComponent } from '../components/poll-edit-result/poll-edit-result.component'; import { PollFormComponent } from '../components/poll-form/poll-form.component'; @@ -42,21 +40,17 @@ export abstract class BasePollDialogComponent extends BaseUiComponent { protected pollForm = viewChild.required(PollFormComponent); protected pollResultForm = viewChild(PollEditResultComponent); - public get formsValid(): boolean { + public formsValid = computed(() => { if (!this.pollForm) { return false; } - return this.pollForm().pollForm.valid; - } + return this.pollForm().isValid(); + }); public analogPollFormOpen = signal(false); - public isAnalogPoll = rxResource({ - params: () => this.pollForm(), - defaultValue: false, - stream({ params }) { - return params.pollForm.get(`visibility`).valueChanges.pipe(map(v => v === PollVisibility.Manually)); - } + public isAnalogPoll = computed(() => { + return this.pollForm().form.visibility().value() === PollVisibility.Manually; }); protected formBuilder = inject(UntypedFormBuilder); diff --git a/client/src/app/site/pages/meetings/modules/poll/base/base-poll.component.ts b/client/src/app/site/pages/meetings/modules/poll/base/base-poll.component.ts deleted file mode 100644 index 663ffb3598a..00000000000 --- a/client/src/app/site/pages/meetings/modules/poll/base/base-poll.component.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { Directive, inject } from '@angular/core'; -import { Id } from '@app/domain/definitions/key-types'; -import { PollContentObject } from '@app/domain/models/poll'; -import { PollState } from '@app/domain/models/poll/poll-constants'; -import { BaseMeetingComponent } from '@app/site/pages/meetings/base/base-meeting.component'; -import { ViewPoll } from '@app/site/pages/meetings/pages/polls'; -import { ChoiceService } from '@app/ui/modules/choice-dialog'; -import { PromptService } from '@app/ui/modules/prompt-dialog'; -import { TranslateService } from '@ngx-translate/core'; -import { BehaviorSubject, Observable } from 'rxjs'; - -import { PollControllerService } from '../services/poll-controller.service/poll-controller.service'; - -@Directive() -export abstract class BasePollComponent extends BaseMeetingComponent { - private stateChangePendingSubject = new BehaviorSubject(false); - - public readonly stateChangePendingObservable = this.stateChangePendingSubject as Observable; - - public get poll(): ViewPoll { - return this._poll; - } - - protected set poll(poll: ViewPoll) { - this._poll = poll; - this.onAfterUpdatePoll(poll); - } - - public pollStateActions = { - [PollState.Created]: { - icon: `play_arrow`, - css: `start-poll-button` - }, - [PollState.Started]: { - icon: `stop`, - css: `stop-poll-button` - }, - [PollState.Finished]: { - icon: `public`, - css: `publish-poll-button` - } - }; - - public get hideChangeState(): boolean { - return this._poll.isPublished || (this._poll.isCreated && this._poll.isAnalog); - } - - protected _id!: Id; - protected _poll!: ViewPoll; - - protected override translate = inject(TranslateService); - protected promptService = inject(PromptService); - protected choiceService = inject(ChoiceService); - protected repo = inject(PollControllerService); - - public async nextPollState(): Promise { - const currentState: PollState = this._poll.state; - if (currentState === PollState.Created || currentState === PollState.Finished) { - if (this._poll.nextState === `published`) { - this.repo.publish(this._poll); - } else { - await this.changeState(this._poll.nextState); - } - } else if (currentState === PollState.Started) { - const title = this.translate.instant(`Are you sure you want to stop this voting?`); - const STOP_LABEL = this.translate.instant(`Stop`); - const STOP_PUBLISH_LABEL = this.translate.instant(`Stop & publish`); - const STOP_PUBLISH_ANONYMIZE_LABEL = this.translate.instant(`Stop, publish & anonymize`); - const actions = [STOP_LABEL, STOP_PUBLISH_LABEL]; - if (this._poll.live_voting_enabled) { - actions.push(STOP_PUBLISH_ANONYMIZE_LABEL); - } - const choice = await this.choiceService.open({ title, multiSelect: false, actions }); - - if (choice?.action === STOP_LABEL) { - await this.changeState(PollState.Finished); - } else if (choice?.action === STOP_PUBLISH_LABEL) { - await this.repo.publish(this.poll).catch(this.raiseError); - } else if (choice?.action === STOP_PUBLISH_ANONYMIZE_LABEL) { - await this.repo.anonymize(this.poll, true).catch(this.raiseError); - } - } - } - - private async changeState(targetState: PollState): Promise { - this.stateChangePendingSubject.next(true); - await this.repo.changePollState(this._poll, targetState).catch(this.raiseError); - this.stateChangePendingSubject.next(false); - } - - public async resetState(): Promise { - const title = this.translate.instant(`Are you sure you want to reset this vote?`); - const content = this.translate.instant(`All votes will be lost.`); - if (await this.promptService.open(title, content)) { - this.changeState(PollState.Created); - } - } - - /** - * Handler for the 'delete poll' button - */ - public async deletePoll(): Promise { - const title = this.translate.instant(`Are you sure you want to delete this vote?`); - const content = this._poll.getTitle(); - if (await this.promptService.open(title, content)) { - await this.repo.delete(this._poll); - } - } - - protected initializePoll(id: Id): void { - this._id = id; - this.loadPoll(this._id); - } - - public async pseudoanonymizePoll(): Promise { - const title = this.translate.instant(`Are you sure you want to anonymize all votes? This cannot be undone.`); - if (await this.promptService.open(title)) { - this.repo.anonymize(this.poll).catch(this.raiseError); - } - } - - /** - * Hook to listen to changes. A poll is already available. - */ - protected onAfterUpdatePoll(_poll: ViewPoll): void {} - - protected loadPoll(_id: Id): void { - this.subscriptions.push( - this.repo.getViewModelObservable(this._id).subscribe(poll => { - if (poll) { - this.poll = poll; - } - }) - ); - } - - public abstract getDetailLink(): string; -} diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-config-form-base.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-config-form-base.component.ts index 242b2897db9..d8c6410c79a 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-config-form-base.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-config-form-base.component.ts @@ -1,4 +1,5 @@ -import { Component, computed, effect, inject, input } from '@angular/core'; +import { Component, computed, effect, inject, input, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { PollState } from '@app/domain/models/poll'; @@ -10,16 +11,19 @@ export abstract class PollFormBaseComponent { public data = input.required>(); + public formValid = signal(false); + public pollStarted = computed(() => { return this.data().state && this.data().state !== PollState.Created; }); - protected fb = inject(UntypedFormBuilder); + protected readonly fb = inject(UntypedFormBuilder); public constructor() { this.initForm(); effect(this.onDataUpdated.bind(this)); + this.form.valueChanges.pipe(takeUntilDestroyed()).subscribe(() => this.formValid.set(this.form.valid)); } protected abstract initForm(): void; 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 247f6e27cce..10f4d1aea4f 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 @@ -113,7 +113,7 @@

{{ option.title }}

[attr.aria-label]="'Update total ballots' | translate" (click)="updateTotalBallots()" > - calculate + functions } 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 3b022e2056b..ab8de93af62 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 @@ -1,5 +1,5 @@
- @if (!pollStarted()) { + @if (!pollStarted() && !hideMethod()) {
{{ 'Election method' | translate }} @@ -8,19 +8,23 @@ {{ 'Yes/No/Abstain' | translate }} +
} - - {{ '100% base' | translate }} - - @for (option of validPercentBases; track option[0]) { - @if (option[0] !== 'yes_no_abstain' || form.get('allow_abstain').getRawValue()) { - - {{ option[1] | translate }} - +
+ + {{ '100% base' | translate }} + + @for (option of validPercentBases; track option[0]) { + @if (option[0] !== 'yes_no_abstain' || form.get('allow_abstain').getRawValue()) { + + {{ option[1] | translate }} + + } } - } - - + + +
+
diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-form-approval/poll-form-approval.component.scss b/client/src/app/site/pages/meetings/modules/poll/components/poll-form-approval/poll-form-approval.component.scss index e69de29bb2d..5d1fd49df19 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-form-approval/poll-form-approval.component.scss +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-form-approval/poll-form-approval.component.scss @@ -0,0 +1,9 @@ +.info-grid { + display: grid; + column-gap: 1em; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); + + > mat-checkbox { + padding-top: 10px; + } +} 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 36d31e90b1a..2defeb98eee 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 @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -9,6 +9,11 @@ import { _, TranslatePipe } from '@ngx-translate/core'; import { ViewPoll } from '../../../../pages/polls'; import { PollFormBaseComponent } from '../poll-config-form-base.component'; +export interface PollFormApproval { + allow_abstain: boolean; + onehundred_percent_base: ApprovalOnehundredPercentBase; +} + @Component({ selector: 'os-poll-form-approval', imports: [ReactiveFormsModule, MatFormFieldModule, MatSelectModule, MatCheckboxModule, TranslatePipe], @@ -17,6 +22,8 @@ import { PollFormBaseComponent } from '../poll-config-form-base.component'; changeDetection: ChangeDetectionStrategy.OnPush }) export class PollFormApprovalComponent extends PollFormBaseComponent { + public hideMethod = input(false); + public validPercentBases: [ApprovalOnehundredPercentBase, string][] = [ [`yes_no`, _('Yes/No')], [`yes_no_abstain`, _('Yes/No/Abstain')], 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 bbfe3d37a3e..82fb85459d2 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 @@ -1,42 +1,69 @@
@if (!pollStarted()) { - - {{ 'Election method' | translate }} - - {{ 'Yes/No' | translate }} - {{ 'Yes/No/Abstain' | translate }} - - -
+ @if (!hideMethod()) { + + {{ 'Election method' | translate }} + + {{ 'Yes/No' | translate }} + {{ 'Yes/No/Abstain' | translate }} + + + } +
{{ 'Min amount of votes' | translate }} + @if (form.get('min_options_amount').hasError('minGreaterThanMax')) { + {{ 'Amount must not exceed max amount of options' | translate }} + } {{ 'Max amount of votes' | translate }} - - - - {{ 'Max amount of yes votes' | translate }} - + @if (form.get('max_options_amount').hasError('min')) { + {{ 'Amount needs to be at least 1' | translate }} + } + @if (form.get('max_options_amount').hasError('max')) { + {{ 'Amount must not exceed amount of available options' | translate }} + }
+ + @if (maxYesVotesEnabled()) { +
+
+ + {{ 'Max amount of yes votes' | translate }} + + @if (form.get('max_yes_amount').hasError('min')) { + {{ 'Amount needs to be at least 1' | translate }} + } + @if (form.get('max_yes_amount').hasError('max')) { + + {{ 'Amount must not exceed amount of available options' | translate }} + + } + +
+ } } - - {{ '100% base' | translate }} - - @for (option of validPercentBases; track option) { - @if (option[0] !== 'yes_no_abstain' || form.get('allow_abstain').getRawValue()) { - - {{ option[1] | translate }} - +
+ + {{ '100% base' | translate }} + + @for (option of validPercentBases; track option) { + @if (option[0] !== 'yes_no_abstain' || form.get('allow_abstain').getRawValue()) { + + {{ option[1] | translate }} + + } } - } - - + + +
+
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 caeb7839c56..5665ba7e3d8 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,15 +1,25 @@ -import { ChangeDetectionStrategy, Component, effect, input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, effect, inject, input } from '@angular/core'; import { AbstractControl, ReactiveFormsModule, ValidationErrors, ValidatorFn, Validators } from '@angular/forms'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { RatingApprovalOnehundredPercentBase } from '@app/domain/models/poll/poll-config-rating-approval'; +import { RatingScoreOnehundredPercentBase } from '@app/domain/models/poll/poll-config-rating-score'; +import { MeetingSettingsService } from '@app/site/pages/meetings/services/meeting-settings.service'; import { _, TranslatePipe } from '@ngx-translate/core'; import { ViewPoll } from '../../../../pages/polls'; import { PollFormBaseComponent } from '../poll-config-form-base.component'; +export interface PollFormRatingApproval { + max_options_amount: number; + min_options_amount: number; + max_yes_amount: number; + onehundred_percent_base: RatingScoreOnehundredPercentBase; + display_chart: string; +} + @Component({ selector: 'os-poll-form-rating-approval', imports: [ @@ -35,8 +45,13 @@ export class PollFormRatingApprovalComponent extends PollFormBaseComponent { [`disabled`, _('Disabled (no percents)')] ]; + public hideMethod = input(false); public optionAmount = input(null); + private meetingSettingsService = inject(MeetingSettingsService); + + public maxYesVotesEnabled = this.meetingSettingsService.signal(`poll_enable_max_yes_votes`); + protected initForm(): void { this.form = this.fb.group({ onehundred_percent_base: [`valid`], @@ -66,7 +81,12 @@ export class PollFormRatingApprovalComponent extends PollFormBaseComponent { } public getSerialzedForm(): Record { - return this.form.value; + const formValue = this.form.value; + if (!this.maxYesVotesEnabled()) { + delete formValue.max_yes_amount; + } + + return formValue; } private minOptionsAmountValidator(): ValidatorFn { 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 a310e2ff61f..9cc129eb795 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 @@ -1,13 +1,7 @@
@if (!pollStarted()) { -
- - {{ 'General abstain' | translate }} - -
- -
+
@if (!form.get('allow_general_abstain').value) { {{ 'Min amount of votes' | translate }} @@ -23,11 +17,14 @@
-
+
@if (!form.get('allow_general_abstain').value) { {{ 'Min amount of options voted' | translate }} - + + @if (form.get('min_options_amount').hasError('minGreaterThanMax')) { + {{ 'Amount must not exceed max amount of options' | translate }} + } } @else {
@@ -36,26 +33,42 @@ {{ 'Max amount of options voted' | translate }} + @if (form.get('max_options_amount').hasError('min')) { + {{ 'Amount needs to be at least 1' | translate }} + } + @if (form.get('max_options_amount').hasError('max')) { + {{ 'Amount must not exceed amount of available options' | translate }} + }
+
{{ 'Max votes per option' | translate }}
+ +
+ + {{ 'General abstain' | translate }} + +
} - - {{ '100% base' | translate }} - - @for (option of validPercentBases; track option) { - - {{ option[1] | translate }} - - } - - +
+ + {{ '100% base' | translate }} + + @for (option of validPercentBases; track option) { + + {{ option[1] | translate }} + + } + + +
+
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 10814bcc6c5..507e05ccd2a 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 @@ -10,6 +10,15 @@ import { _, TranslatePipe } from '@ngx-translate/core'; import { ViewPoll } from '../../../../pages/polls'; import { PollFormBaseComponent } from '../poll-config-form-base.component'; +export interface PollFormRatingScore { + max_options_amount: number; + min_options_amount: number; + max_votes_per_option: number; + max_vote_sum: number; + min_vote_sum: number; + onehundred_percent_base: RatingScoreOnehundredPercentBase; +} + @Component({ selector: 'os-poll-form-rating-score', imports: [ 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 f40ac83155e..064cca85029 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 @@ -1,5 +1,5 @@
- @if (!pollStarted()) { + @if (!pollStarted() && !hideMethod()) {
{{ 'Election method' | translate }} @@ -13,20 +13,6 @@
@if (!pollStarted()) { -
- - {{ 'General abstain' | translate }} - - - - @if (form.get('strike_out').value) { - {{ 'General approval' | translate }} - } @else { - {{ 'General rejection' | translate }} - } - -
-
@if (!form.get('allow_general_abstain').value) { @@ -42,6 +28,20 @@
+ +
+ + {{ 'General abstain' | translate }} + + + + @if (form.get('strike_out').valueChanges | async) { + {{ 'General approval' | translate }} + } @else { + {{ 'General rejection' | translate }} + } + +
}
@@ -52,6 +52,7 @@ @if ( option[0] !== 'no_general' || form.get('allow_nota').getRawValue() || + form.get('allow_general_abstain').getRawValue() || form.get('min_options_amount').getRawValue() === 0 ) { 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 afc7273c375..9d2aabcb744 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,3 +1,4 @@ +import { AsyncPipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, effect, input } from '@angular/core'; import { AbstractControl, ReactiveFormsModule, ValidationErrors, ValidatorFn, Validators } from '@angular/forms'; import { MatCheckboxModule } from '@angular/material/checkbox'; @@ -9,9 +10,18 @@ import { _, TranslatePipe } from '@ngx-translate/core'; import { ViewPoll } from '../../../../pages/polls'; import { PollFormBaseComponent } from '../poll-config-form-base.component'; +export interface PollFormSelection { + max_options_amount: number; + min_options_amount: number; + allow_nota: boolean; + onehundred_percent_base: SelectionOnehundredPercentBase; + strike_out: boolean; + display_chart: string; +} + @Component({ selector: 'os-poll-form-selection', - imports: [ReactiveFormsModule, MatCheckboxModule, MatInputModule, MatSelectModule, TranslatePipe], + imports: [ReactiveFormsModule, MatCheckboxModule, MatInputModule, MatSelectModule, TranslatePipe, AsyncPipe], templateUrl: './poll-form-selection.component.html', styleUrls: [`../poll-form/poll-form.component.scss`, './poll-form-selection.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush @@ -26,6 +36,7 @@ export class PollFormSelectionComponent extends PollFormBaseComponent { [`disabled`, _('Disabled (no percents)')] ]; + public hideMethod = input(false); public optionAmount = input(null); public getSerialzedForm(): Record { diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.html b/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.html index 8e70ae53fde..a992efec189 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.html @@ -1,21 +1,21 @@
- + {{ 'Title' | translate }}

- +

- @if (!data() || !data().state || data().isCreated) { -
- - @if (isEVotingEnabled()) { + @if (!data() || isCreated()) { + @if (isEVotingEnabled()) { +
+ {{ 'Visibility' | translate }} - + @for (entry of visibilityOptions | keyvalue; track entry.value) { {{ entry.value | translateKey: 'poll_visibility' }} @@ -27,14 +27,12 @@

} {{ 'This field is required.' | translate }} - } - - @if (isEVotingSelected) { + {{ 'Entitled to vote' | translate }} [sortFn]="sortFn" > - } -

+
+ } -
+
- @if (isLiveVotingAvailable) { - + @if (isLiveVotingAvailable()) { + {{ 'Live voting' | translate }} @@ -57,17 +55,92 @@

}

} + + @if (!customConfigForm()) { +
+
+ + {{ 'Election method' | translate }} + + @if (optionAmount() <= 1 && form.options().value().length <= 1) { + {{ 'Yes/No' | translate }} + + {{ 'Yes/No/Abstain' | translate }} + + } + + @if (optionAmount() > 1 || form.options().value().length > 1) { + @if (optionType() === 'text') { + + {{ 'Yes per option' | translate }} + + @if (allowCumulative()) { + + {{ 'Amount per option' | translate }} + + } + } @else if (optionType() === 'meeting_user') { + + {{ 'Yes per candidate' | translate }} + + + {{ 'No per candidate' | translate }} + + + {{ 'Yes/No per candidate' | translate }} + + + {{ 'Yes/No/Abstain per candidate' | translate }} + + @if (allowCumulative()) { + + {{ 'Amount per candidate' | translate }} + + } + + + {{ 'Yes/No per list' | translate }} + + + {{ 'Yes/No/Abstain per list' | translate }} + + } + } + + {{ 'This field is required.' | translate }} + +
+
+
+ }
- + +
+ @switch (this.selectedMethod()) { + @case ('approval') { + + } + @case ('selection') { + + } + @case ('rating_approval') { + + } + @case ('rating_score') { + + } + } +
+
@if (optionEdit()) {

{{ 'Options' | translate }}

diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.scss b/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.scss index fa1ab1a2bd3..e2ee7a0bcd9 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.scss +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.scss @@ -67,6 +67,10 @@ grid-column: 2 / 4; grid-row: 2 / 3; } + + .child-options { + margin-left: 15px; + } } .percent-base-panel { 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 a25b66d345b..332d2d4a6c4 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 @@ -1,33 +1,58 @@ import { KeyValuePipe } from '@angular/common'; -import { Component, effect, inject, input, OnInit, ViewEncapsulation } from '@angular/core'; -import { AbstractControl, ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Component, computed, effect, inject, input, signal, viewChild, ViewEncapsulation } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { form, FormField, FormRoot, required } from '@angular/forms/signals'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatDialog } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; +import { Ids } from '@app/domain/definitions/key-types'; import { PollVisibility } from '@app/domain/models/poll'; import { infoDialogSettings } from '@app/infrastructure/utils/dialog-settings'; import { BaseComponent } from '@app/site/base/base.component'; +import { MeetingSettingsService } from '@app/site/pages/meetings/services/meeting-settings.service'; import { DirectivesModule } from '@app/ui/directives'; import { EditableListComponent } from '@app/ui/modules/editable-list'; import { SearchSelectorModule } from '@app/ui/modules/search-selector'; import { PipesModule } from '@app/ui/pipes'; import { TranslatePipe } from '@ngx-translate/core'; -import { combineLatest, startWith } from 'rxjs'; import { GroupControllerService, ViewGroup } from '../../../../pages/participants'; import { ViewPoll } from '../../../../pages/polls'; +import { PollFormApprovalComponent } from '../poll-form-approval/poll-form-approval.component'; +import { PollFormRatingApprovalComponent } from '../poll-form-rating-approval/poll-form-rating-approval.component'; +import { PollFormRatingScoreComponent } from '../poll-form-rating-score/poll-form-rating-score.component'; +import { PollFormSelectionComponent } from '../poll-form-selection/poll-form-selection.component'; import { VotingPrivacyWarningDialogComponent } from '../voting-privacy-warning/voting-privacy-warning-dialog.component'; +interface PollForm { + title: string; + visibility: PollVisibility; + entitled_group_ids: Ids; + live_voting_enabled: boolean; + option_type: 'meeting_user' | 'text'; + options: any[]; + method: 'approval' | 'selection' | 'rating_approval' | 'rating_score'; + method_preselection: string | null; +} + @Component({ selector: `os-poll-form`, templateUrl: `./poll-form.component.html`, styleUrls: [`./poll-form.component.scss`], imports: [ + PollFormApprovalComponent, + PollFormSelectionComponent, + PollFormRatingApprovalComponent, + PollFormRatingScoreComponent, EditableListComponent, TranslatePipe, + FormField, + FormRoot, MatInputModule, + MatIconModule, MatFormFieldModule, MatCheckboxModule, MatSelectModule, @@ -39,13 +64,18 @@ import { VotingPrivacyWarningDialogComponent } from '../voting-privacy-warning/v ], encapsulation: ViewEncapsulation.None }) -export class PollFormComponent extends BaseComponent implements OnInit { - public pollForm: UntypedFormGroup; +export class PollFormComponent extends BaseComponent { + private approvalForm = viewChild(PollFormApprovalComponent); + private selectionForm = viewChild(PollFormSelectionComponent); + private ratingApprovalForm = viewChild(PollFormRatingApprovalComponent); + private ratingScoreForm = viewChild(PollFormRatingScoreComponent); public readonly visibilityOptions = PollVisibility; public showNonNominalWarning = false; + public customConfigForm = input(false); + public optionAmount = input(0); public optionType = input<'meeting_user' | 'text'>('text'); public optionEdit = input(false); public isEVotingEnabled = input.required(); @@ -54,61 +84,99 @@ export class PollFormComponent extends BaseComponent implements OnInit { public readonly data = input>({}); - public get isCreated(): boolean { + private pollModel = signal({ + title: ``, + visibility: PollVisibility.Open, + entitled_group_ids: [], + live_voting_enabled: false, + option_type: 'text', + options: [], + method: null, + method_preselection: `` + }); + + public form = form(this.pollModel, schemaPath => { + required(schemaPath.title); + required(schemaPath.visibility); + if (!this.customConfigForm()) { + required(schemaPath.method_preselection); + } + }); + + public isValid = computed(() => { + return this.form().valid() && this.methodForm()?.formValid(); + }); + + public isCreated = computed(() => { return !this.data()?.state || this.data().isCreated; - } + }); - public get isOpenVotingSelected(): boolean { - return this.pollTypeControl?.value === PollVisibility.Open || false; - } + public selectedMethod = computed(() => { + const preselection = this.form.method_preselection().value(); + if (!preselection) { + return null; + } - public get isNamedVotingSelected(): boolean { - return this.pollTypeControl?.value === PollVisibility.Named || false; - } + return this.form.method_preselection().value().split(`.`)[0]; + }); - public get isEVotingSelected(): boolean { - return this.isEVotingEnabled() && this.pollTypeControl?.value !== PollVisibility.Manually; - } + public methodForm = computed(() => { + switch (this.selectedMethod()) { + case `approval`: + return this.approvalForm(); + case `selection`: + return this.selectionForm(); + case `rating_approval`: + return this.ratingApprovalForm(); + case `rating_score`: + return this.ratingScoreForm(); + } - public get isLiveVotingAvailable(): boolean { - return this.isEVotingSelected && (this.isNamedVotingSelected || this.isOpenVotingSelected); - } + return null; + }); - private get pollTypeControl(): AbstractControl { - return this.pollForm.get(`visibility`); - } + public methodConfig = computed(() => { + if (this.methodForm()) { + return this.methodForm().getSerialzedForm(); + } - private get liveVotingControl(): AbstractControl { - return this.pollForm.get(`live_voting_enabled`); - } + return null; + }); + + public isOpenVotingSelected = computed(() => { + return this.form.visibility().value() === PollVisibility.Open || false; + }); + + public isNamedVotingSelected = computed(() => { + return this.form.visibility().value() === PollVisibility.Named || false; + }); + + public isEVotingSelected = computed(() => { + return this.isEVotingEnabled() && this.form.visibility().value() !== PollVisibility.Manually; + }); + + public isLiveVotingAvailable = computed(() => { + return this.isEVotingSelected() && (this.isNamedVotingSelected() || this.isOpenVotingSelected()); + }); - private fb = inject(UntypedFormBuilder); public groupRepo = inject(GroupControllerService); private dialog = inject(MatDialog); + private meetingSettingsService = inject(MeetingSettingsService); + + public allowCumulative = this.meetingSettingsService.signal(`poll_enable_max_votes_per_option`); public constructor() { super(); - this.initContentForm(); - - effect(() => { - this.updateData(); - }); - } - public ngOnInit(): void { - this.subscriptions.push( - combineLatest([ - this.pollForm.valueChanges.pipe(startWith(``)), - this.pollTypeControl.valueChanges.pipe(startWith(``)) - ]).subscribe(() => { - this.updateLiveVotingEnabled(); - this.setWarning(); - }) - ); + effect(this.updateLiveVotingEnabled.bind(this)); + effect(this.setWarning.bind(this)); + effect(this.updateData.bind(this)); + effect(this.updateConfigData.bind(this)); + effect(this.changeMethod.bind(this)); } public getValues(): Partial<{ [place in keyof ViewPoll]: any }> { - return { ...this.data, ...this.serializeForm(this.pollForm) }; + return { ...this.data, ...this.serializeForm() }; } public openVotingWarning(event: MouseEvent): void { @@ -117,54 +185,87 @@ export class PollFormComponent extends BaseComponent implements OnInit { } public onOptionsChange(items: string[]): void { - this.pollForm.get('options').setValue(items); + this.form.options().value.set(items); } private updateLiveVotingEnabled(): void { - if (!this.isLiveVotingAvailable) { - this.liveVotingControl.setValue(false, { emitEvent: false }); + if (!this.isLiveVotingAvailable()) { + this.form.live_voting_enabled().value.set(false); } } private setWarning(): void { - this.showNonNominalWarning = this.pollTypeControl.value === PollVisibility.Secret; + this.showNonNominalWarning = this.pollModel().visibility === PollVisibility.Secret; } - private serializeForm(formGroup: UntypedFormGroup): Partial { + private serializeForm(): Partial { // getRawValue() includes disabled controls - return { ...formGroup.getRawValue() }; + return { ...this.pollModel() }; } - private initContentForm(): void { - this.pollForm = this.fb.group({ - title: [``, Validators.required], - visibility: [PollVisibility.Open, Validators.required], - entitled_group_ids: [], - live_voting_enabled: [false], - option_type: ['text'], - options: [[]] - }); + private updateData(): void { + const data = this.data(); + if (data && this.form) { + if (data.entitled_group_ids !== undefined) + this.form['entitled_group_ids']().value.set(data.entitled_group_ids); + if (data.live_voting_enabled !== undefined) + this.form['live_voting_enabled']().value.set(!!data.live_voting_enabled); + if (data.title !== undefined) this.form['title']().value.set(data.title); + if (data.visibility !== undefined) this.form['visibility']().value.set(data.visibility); + if (data.options !== undefined && !data.options.some(option => option.meeting_user_id)) + this.form['options']().value.set(data.options.map(option => option.text)); + if (data.config?.method) { + let preselection = data.config.method; + if (!(data instanceof ViewPoll) && this.optionAmount() <= 1 && data.config.method !== `rating_score`) { + preselection = `approval`; + } + + if (preselection === `approval` || preselection === `rating_approval`) { + preselection += data.config.allow_abstain ? `.yes_no_abstain` : `.yes_no`; + } else if (preselection == `selection`) { + preselection += data.config.strike_out ? `.no` : `.yes`; + } + + this.form['method_preselection']().value.set(preselection); + } + } } - private updateData(): void { + private updateConfigData(): void { + const configForm = this.methodForm(); + if (!configForm) { + return; + } + const data = this.data(); - if (data && this.pollForm) { - const patch: Record = {}; + if (data && this.form) { + for (const field of Object.keys(data.config)) { + const ctrl = this.methodForm().form.get(field); + const val = data.config[field]; + if (ctrl && val !== undefined) { + ctrl.patchValue(val); + } + } + } + } - if (data.entitled_group_ids !== undefined) patch['entitled_group_ids'] = data.entitled_group_ids; - if (data.live_voting_enabled !== undefined) patch['live_voting_enabled'] = !!data.live_voting_enabled; - if (data.title !== undefined) patch['title'] = data.title; - if (data.visibility !== undefined) patch['visibility'] = data.visibility; - if (data.options !== undefined && !data.options.some(option => option.meeting_user_id)) - patch['options'] = data.options.map(option => option.text); - if (data.config?.allow_abstain !== undefined) patch['allow_abstain'] = data.config.allow_abstain; - if (data.config?.allow_nota !== undefined) patch['allow_nota'] = data.config.allow_nota; - if (data.config?.strike_out !== undefined) patch['strike_out'] = data.config.strike_out; - if (data.config?.display_chart !== undefined) patch['display_chart'] = data.config.display_chart; - if (data.config?.onehundred_percent_base !== undefined) - patch['onehundred_percent_base'] = data.config.onehundred_percent_base; - - this.pollForm.patchValue(patch); + private changeMethod(): void { + const configForm = this.methodForm(); + if (!configForm) { + return; + } + + const mode = this.form.method_preselection().value().split(`.`)[1]; + if (this.selectedMethod() === `approval` || this.selectedMethod() === `rating_approval`) { + this.methodForm() + .form.get(`allow_abstain`) + .patchValue(mode === `yes_no_abstain`); + } + + if (this.selectedMethod() === `selection`) { + this.methodForm() + .form.get(`strike_out`) + .patchValue(mode === `no`); } } } 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 88d6e47e535..ea25270c2a9 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 @@ -127,7 +127,7 @@ } } - @if (config().max_yes_amount > 1 && config().max_yes_amount < config().max_vote_sum) { + @if (config().max_yes_amount >= 1 && config().max_yes_amount < config().max_vote_sum) {
{{ 'Maximum amount of yes votes' | translate }}: {{ config().max_yes_amount }} diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-result-approval/poll-result-approval.component.scss b/client/src/app/site/pages/meetings/modules/poll/components/poll-result-approval/poll-result-approval.component.scss index 84a234c92c7..15e25a5ea64 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-result-approval/poll-result-approval.component.scss +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-result-approval/poll-result-approval.component.scss @@ -21,7 +21,7 @@ border-bottom: 0; } tr.votesvalid { - border-top: 1px solid rgba(255, 255, 255, 0.12); + border-top: 2px solid rgba(128, 128, 128, 0.2); } } } diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-result-base.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-result-base.component.ts index fc9fdafa24f..e350110d522 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-result-base.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-result-base.component.ts @@ -1,11 +1,11 @@ -import { Component, computed, input } from '@angular/core'; +import { computed, Directive, input } from '@angular/core'; import { BaseViewModel } from '@app/site/base/base-view-model'; import { ViewPoll, ViewPollOption } from '../../../pages/polls'; export const PERCENT_DECIMAL_PLACES = 3; -@Component({ template: `` }) +@Directive() export abstract class PollResultBaseComponent { public poll = input.required(); 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 1d705aaa57c..e55d6d6b08d 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,5 +1,5 @@ import { NgTemplateOutlet } from '@angular/common'; -import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core'; +import { Component, computed, inject, signal } from '@angular/core'; import { VOTE_MAJORITY } from '@app/domain/models/poll'; import { ThemeService } from '@app/site/services/theme.service'; import { IconContainerComponent } from '@app/ui/modules/icon-container'; @@ -53,8 +53,7 @@ const PollChartBarThickness = 20; NgTemplateOutlet ], templateUrl: './poll-result-selection.component.html', - styleUrl: './poll-result-selection.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush + styleUrl: './poll-result-selection.component.scss' }) export class PollResultSelectionComponent extends PollResultBaseComponent< ViewPollConfigSelection, diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-result/poll-result.component.html b/client/src/app/site/pages/meetings/modules/poll/components/poll-result/poll-result.component.html index 773b33b9f1b..d334e2b7437 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-result/poll-result.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-result/poll-result.component.html @@ -1,14 +1,14 @@ @switch (configType()) { - @case ('poll_config_approval') { + @case ('approval') { } - @case ('poll_config_rating_approval') { + @case ('rating_approval') { } - @case ('poll_config_rating_score') { + @case ('rating_score') { } - @case ('poll_config_selection') { + @case ('selection') { } } diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-result/poll-result.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-result/poll-result.component.ts index a73bb3a2cbf..bc7070a1760 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-result/poll-result.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-result/poll-result.component.ts @@ -1,5 +1,4 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; -import { collectionFromFqid } from '@app/infrastructure/utils/transform-functions'; import { ViewPoll } from '../../../../pages/polls'; import { PollResultApprovalComponent } from '../poll-result-approval/poll-result-approval.component'; @@ -23,10 +22,6 @@ export class PollResultComponent { public poll = input.required(); public configType = computed(() => { - if (!this.poll().config_id) { - return `none`; - } - - return collectionFromFqid(this.poll().config_id); + return this.poll().config?.method ?? `none`; }); } diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-stop-dialog/poll-stop-dialog.component.html b/client/src/app/site/pages/meetings/modules/poll/components/poll-stop-dialog/poll-stop-dialog.component.html new file mode 100644 index 00000000000..1b32b0466cc --- /dev/null +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-stop-dialog/poll-stop-dialog.component.html @@ -0,0 +1,36 @@ +

{{ 'Are you sure you want to stop this voting?' | translate }}

+ + +
+ +
+ +
+ +
+ + @if (poll.isOpen) { +
+ +
+ } +
+ + + + diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-stop-dialog/poll-stop-dialog.component.scss b/client/src/app/site/pages/meetings/modules/poll/components/poll-stop-dialog/poll-stop-dialog.component.scss new file mode 100644 index 00000000000..15f54534dfc --- /dev/null +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-stop-dialog/poll-stop-dialog.component.scss @@ -0,0 +1,9 @@ +@use 'src/assets/styles/poll-styles-common.scss'; + +.poll-action-row:not(:last-of-type) { + margin-bottom: 28px; +} + +.poll-action-row { + text-align: center; +} diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-stop-dialog/poll-stop-dialog.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-stop-dialog/poll-stop-dialog.component.ts new file mode 100644 index 00000000000..fb0c8ac5681 --- /dev/null +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-stop-dialog/poll-stop-dialog.component.ts @@ -0,0 +1,44 @@ +import { Component, inject } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { + MAT_DIALOG_DATA, + MatDialogActions, + MatDialogClose, + MatDialogContent, + MatDialogRef, + MatDialogTitle +} from '@angular/material/dialog'; +import { MatIconModule } from '@angular/material/icon'; +import { ViewPoll } from '@app/site/pages/meetings/pages/polls'; +import { TranslatePipe } from '@ngx-translate/core'; + +export interface DialogData { + poll: ViewPoll; +} + +@Component({ + selector: 'os-poll-stop-dialog', + templateUrl: './poll-stop-dialog.component.html', + styleUrl: './poll-stop-dialog.component.scss', + imports: [ + MatButtonModule, + MatIconModule, + MatDialogTitle, + MatDialogContent, + MatDialogActions, + MatDialogClose, + TranslatePipe + ] +}) +export class PollStopDialog { + private readonly dialogRef = inject(MatDialogRef); + private readonly data = inject(MAT_DIALOG_DATA); + + public get poll(): ViewPoll { + return this.data.poll; + } + + public onNoClick(): void { + this.dialogRef.close(); + } +} diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-option/poll-vote-option.component.scss b/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-option/poll-vote-option.component.scss index 87457c9858c..297e90bc7c8 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-option/poll-vote-option.component.scss +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-option/poll-vote-option.component.scss @@ -1,4 +1,3 @@ .user-subtitle { - color: #ffffffb3; font-weight: 400; } diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-selection/poll-vote-selection.component.html b/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-selection/poll-vote-selection.component.html index b6caeafbfbd..d7cf22272b0 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-selection/poll-vote-selection.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-selection/poll-vote-selection.component.html @@ -1,4 +1,4 @@ -
+
@let multiVotes = !!(config()?.max_vote_sum > 1);
@@ -61,7 +61,12 @@ @for (option of options(); track option.id; let i = $index) { -
+
@@ -87,7 +92,7 @@
@if (config().allow_nota) { -
+
@let optionName = config().strike_out ? ('General approval' | translate) : ('General rejection' | translate);
@@ -106,7 +111,7 @@ } @if (config().min_vote_sum === 0) { -
+
{{ 'General abstain' | translate }}
diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-selection/poll-vote-selection.component.scss b/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-selection/poll-vote-selection.component.scss index ffe7654ea92..0b28a116f3d 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-selection/poll-vote-selection.component.scss +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-selection/poll-vote-selection.component.scss @@ -51,6 +51,11 @@ } } +.is-strikeout .option-row:not(.general-option).is-selected .vote-option-title { + color: var(--theme-no); + text-decoration: line-through; +} + .vote-option-title { display: flex; align-items: center; 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 0ab991aee19..d4862077154 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 @@ -40,9 +40,16 @@ {{ 'Voting in progress' | translate }} } @else if (poll().isFinished && poll().published) { {{ 'published' | translate }} + @if (poll().isOpen && poll().isAnonymized) { + ({{ 'anonymized' | translate }}) + } } @else { {{ poll().state | translateKey: 'poll_state' }} } + + @if (poll().isFinished && !poll().published) { + ({{ 'unpublished' | translate }}) + }
@@ -57,9 +64,10 @@
-
- @if (pollStateAction()) { +
+ @if (pollStateAction() && !hideChangeState()) { } + + @if (poll().canAnonymize) { + + }
@if (!poll().isFinished && poll().visibility !== 'manually') { @@ -104,7 +130,7 @@ } } } -} @else if (poll().isFinished) { +} @else if (poll().isFinished && poll().result) { } @@ -126,14 +152,6 @@ [menuItem]="true" [object]="poll().getProjectionBuildDescriptor()" /> - @@ -53,7 +34,7 @@
- @if (isAnalogPoll.value()) { + @if (isAnalogPoll()) { @if (analogPollFormOpen()) { @@ -86,7 +34,7 @@
- @if (isAnalogPoll.value()) { + @if (isAnalogPoll()) { @if (analogPollFormOpen()) { @@ -30,7 +30,7 @@
- @if (isAnalogPoll.value()) { + @if (isAnalogPoll()) { @if (analogPollFormOpen()) {