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 be5ec9ecf0..24b39189e1 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/components/poll-config-form-base.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-config-form-base.component.ts index 242b2897db..d8c6410c79 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-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 3b022e2056..ab8de93af6 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 e69de29bb2..5d1fd49df1 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 36d31e90b1..2defeb98ee 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 bbfe3d37a3..18edc2d996 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,14 +1,16 @@
@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 }} @@ -18,7 +20,10 @@ {{ 'Max amount of votes' | translate }} +
+
+
{{ 'Max amount of yes votes' | translate }} @@ -26,17 +31,20 @@
} - - {{ '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 caeb7839c5..c26adfa5f5 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 @@ -5,11 +5,20 @@ 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 { _, 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,6 +44,7 @@ export class PollFormRatingApprovalComponent extends PollFormBaseComponent { [`disabled`, _('Disabled (no percents)')] ]; + public hideMethod = input(false); public optionAmount = input(null); protected initForm(): void { 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 a310e2ff61..625582fb7d 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,7 +17,7 @@
-
+
@if (!form.get('allow_general_abstain').value) { {{ 'Min amount of options voted' | translate }} @@ -40,22 +34,32 @@
+
{{ '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 10814bcc6c..507e05ccd2 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 f40ac83155..7bb516f5e3 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').value) { + {{ 'General approval' | translate }} + } @else { + {{ 'General rejection' | 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 afc7273c37..295b139288 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 @@ -9,6 +9,15 @@ 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], @@ -26,6 +35,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 8e70ae53fd..c714b6b43b 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,99 @@

}

} + + @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 }} + + {{ 'No per option' | translate }} + + {{ 'Yes/No per option' | translate }} + + + {{ 'Yes/No/Abstain per option' | translate }} + + @if (allowCumulative()) { + + {{ 'Amount per option' | translate }} + + } + } @else { + + {{ '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 }} + + } + } + } + + @if (optionAmount() > 1 || form.options().value().length > 1) { + {{ '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.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component.ts index a25b66d345..7f82d65bee 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,60 @@ 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 { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +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 { collectionFromFqid } from '@app/infrastructure/utils/transform-functions'; 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 +66,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 +86,117 @@ export class PollFormComponent extends BaseComponent implements OnInit { public readonly data = input>({}); - public get isCreated(): boolean { + public allowCumulative = signal(false); + private pollModel = signal({ + title: ``, + visibility: PollVisibility.Open, + entitled_group_ids: [], + live_voting_enabled: false, + option_type: 'text', + options: [], + method: null, + method_preselection: `selection.yes` + }); + + 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 constructor() { super(); - this.initContentForm(); effect(() => { - this.updateData(); + this.updateLiveVotingEnabled(); + this.setWarning(); }); - } - public ngOnInit(): void { - this.subscriptions.push( - combineLatest([ - this.pollForm.valueChanges.pipe(startWith(``)), - this.pollTypeControl.valueChanges.pipe(startWith(``)) - ]).subscribe(() => { - this.updateLiveVotingEnabled(); - this.setWarning(); - }) - ); + effect(this.updateData.bind(this)); + effect(this.changeMethod.bind(this)); + + this.allowCumulative.set(this.meetingSettingsService.instant(`poll_enable_max_votes_per_option`)); + let method = this.data()?.config?.method; + if (this.data()?.config_id) { + const collection = collectionFromFqid(this.data()?.config_id); + method = collection.replace(`poll_config_`, ``); + } + if (method === `rating_score`) { + this.allowCumulative.set(true); + } + + this.meetingSettingsService + .get(`poll_enable_max_votes_per_option`) + .pipe(takeUntilDestroyed()) + .subscribe(v => { + this.allowCumulative.set(v); + }); } 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 +205,63 @@ 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() }; - } - - 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: [[]] - }); + return { ...this.pollModel() }; } private updateData(): void { const data = this.data(); - if (data && this.pollForm) { + if (data && this.form) { const patch: Record = {}; - 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.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)) - patch['options'] = data.options.map(option => option.text); + this.form['options']().value.set(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; + } + } + + 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`); + } - this.pollForm.patchValue(patch); + if (this.selectedMethod() === `selection`) { + this.methodForm() + .form.get(`strike_out`) + .patchValue(mode === `no`); } } } diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-dialog/topic-poll-dialog.component.html b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-dialog/topic-poll-dialog.component.html index 25f16b0ca5..7a35b2e5f9 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-dialog/topic-poll-dialog.component.html +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-dialog/topic-poll-dialog.component.html @@ -1,38 +1,19 @@
- - @if (pollData.isCreated === false) { - @switch (this.getSelectedMethod()) { - @case ('approval') { - - } - @case ('selection') { - - } - } - } @else { - - -
- -
-
- -
- -
-
-
- } -
+
@if (analogPollFormOpen()) {
@@ -43,7 +24,7 @@
- @@ -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()) {