From 9bc4dd9f400a58d19858b743d012128abd6a8007 Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Tue, 22 Sep 2026 18:04:07 +0200 Subject: [PATCH 01/26] Hide method field on edit if poll started --- .../poll-form/poll-form.component.html | 102 +++++++++--------- 1 file changed, 51 insertions(+), 51 deletions(-) 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 a992efec18..96fe01ae2c 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 @@ -54,64 +54,64 @@

} - } - - @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 (!customConfigForm()) { +
+
+ + {{ 'Election method' | translate }} + + @if (optionAmount() <= 1 && form.options().value().length <= 1) { + {{ 'Yes/No' | translate }} + + {{ 'Yes/No/Abstain' | translate }} - @if (allowCumulative()) { - - {{ 'Amount per option' | translate }} + } + + @if (optionAmount() > 1 || form.options().value().length > 1) { + @if (optionType() === 'text') { + + {{ 'Yes 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 }} + @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 }} - + + {{ 'Yes/No per list' | translate }} + + + {{ 'Yes/No/Abstain per list' | translate }} + + } } - } - - {{ 'This field is required.' | translate }} - -
+ + {{ 'This field is required.' | translate }} + +
+
-
+ } }
From f29233e3d987c19001c163dd49a4a329fb9baa73 Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Tue, 22 Sep 2026 18:04:26 +0200 Subject: [PATCH 02/26] Fix poll result if on no votes --- .../poll-result-approval/poll-result-approval.component.ts | 2 +- .../meetings/pages/polls/view-models/poll-config-approval.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 aceb895213..c03bbab4cc 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 @@ -94,7 +94,7 @@ export class PollResultApprovalComponent extends PollResultBaseComponent Date: Wed, 23 Sep 2026 10:27:03 +0200 Subject: [PATCH 03/26] Update meta --- client/src/app/domain/models/poll/poll-entitled-user.ts | 8 +++++++- client/src/app/domain/models/poll/poll.ts | 2 ++ .../meeting-settings-definitions.ts | 6 +++--- meta | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/client/src/app/domain/models/poll/poll-entitled-user.ts b/client/src/app/domain/models/poll/poll-entitled-user.ts index 65ee1d3c7b..14085cb65c 100644 --- a/client/src/app/domain/models/poll/poll-entitled-user.ts +++ b/client/src/app/domain/models/poll/poll-entitled-user.ts @@ -6,10 +6,16 @@ export class PollEntitledUser extends BaseModel { public poll_id!: Id; public meeting_user_id: Id; + public present: boolean; public constructor(input?: Partial) { super(PollEntitledUser.COLLECTION, input); } - public static readonly REQUESTABLE_FIELDS: (keyof PollEntitledUser)[] = [`id`, `poll_id`, `meeting_user_id`]; + public static readonly REQUESTABLE_FIELDS: (keyof PollEntitledUser)[] = [ + `id`, + `poll_id`, + `meeting_user_id`, + `present` + ]; } diff --git a/client/src/app/domain/models/poll/poll.ts b/client/src/app/domain/models/poll/poll.ts index 18c48946fa..b0178be9ab 100644 --- a/client/src/app/domain/models/poll/poll.ts +++ b/client/src/app/domain/models/poll/poll.ts @@ -19,6 +19,7 @@ export class Poll extends BaseModel { public result!: string; public anonymized!: boolean; public published!: boolean; + public allow_empty!: boolean; public allow_invalid!: boolean; public allow_vote_split!: boolean; public option_ids!: Id[]; @@ -42,6 +43,7 @@ export class Poll extends BaseModel { `result`, `published`, `anonymized`, + `allow_empty`, `allow_invalid`, `allow_vote_split`, `live_voting_enabled`, diff --git a/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definitions.ts b/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definitions.ts index 9e76808e5d..6391f75679 100644 --- a/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definitions.ts +++ b/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definitions.ts @@ -905,7 +905,7 @@ export const meetingSettings: SettingsGroup[] = fillInSettingsDefaults([ label: _(`Default voting type`), type: `choice`, choiceI18nPrefix: 'poll_visibility', - choices: ['manually', 'open', 'secret', 'named'], + choices: ['manually', 'named', 'open', 'secret'], restrictionFn: (orgaSettings, value: any): any => { const isElectronicVotingEnabled = orgaSettings.instant(`enable_electronic_voting`); if (!isElectronicVotingEnabled && typeof value !== `string`) { @@ -1002,7 +1002,7 @@ export const meetingSettings: SettingsGroup[] = fillInSettingsDefaults([ label: _(`Default voting type`), type: `choice`, choiceI18nPrefix: 'poll_visibility', - choices: ['manually', 'open', 'secret', 'named'], + choices: ['manually', 'named', 'open', 'secret'], restrictionFn: (orgaSettings, value: any): any => { const isElectronicVotingEnabled = orgaSettings.instant(`enable_electronic_voting`); if (!isElectronicVotingEnabled && typeof value !== `string`) { @@ -1065,7 +1065,7 @@ export const meetingSettings: SettingsGroup[] = fillInSettingsDefaults([ label: _(`Default voting type`), type: `choice`, choiceI18nPrefix: 'poll_visibility', - choices: ['manually', 'open', 'secret', 'named'], + choices: ['manually', 'named', 'open', 'secret'], restrictionFn: (orgaSettings, value: any): any => { const isElectronicVotingEnabled = orgaSettings.instant(`enable_electronic_voting`); if (!isElectronicVotingEnabled && typeof value !== `string`) { diff --git a/meta b/meta index 0d6c93a564..a11c29cebe 160000 --- a/meta +++ b/meta @@ -1 +1 @@ -Subproject commit 0d6c93a564c38e4ce584a3d888c38cfc0e6d148f +Subproject commit a11c29cebe12c4fcce9ccb78825e7e5b6d1aa743 From 0d287683591b76f0ec0215db9726bd6f95295b05 Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Wed, 23 Sep 2026 13:52:55 +0200 Subject: [PATCH 04/26] Display entitled users for non finished polls --- .../src/app/domain/models/meetings/meeting.ts | 4 +- client/src/app/domain/translations/poll.ts | 3 +- .../poll-form-rating-score.component.html | 3 ++ .../poll-form-selection.component.html | 3 ++ .../services/entitled-user-filter.service.ts | 8 +++ .../poll-entitled-user.component.ts | 52 ++++++++++++++++--- 6 files changed, 64 insertions(+), 9 deletions(-) diff --git a/client/src/app/domain/models/meetings/meeting.ts b/client/src/app/domain/models/meetings/meeting.ts index f4e3e50f77..d1f349f09d 100644 --- a/client/src/app/domain/models/meetings/meeting.ts +++ b/client/src/app/domain/models/meetings/meeting.ts @@ -174,7 +174,9 @@ export class Settings { // 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_required_majority: + `no_majority` | `two_third_majority` | `absolute_majority` | `simple_majority`; + public poll_default_live_voting_enabled: boolean; public poll_enable_max_votes_per_option: boolean; public poll_default_allow_invalid: boolean; diff --git a/client/src/app/domain/translations/poll.ts b/client/src/app/domain/translations/poll.ts index e16ab7e429..982a397866 100644 --- a/client/src/app/domain/translations/poll.ts +++ b/client/src/app/domain/translations/poll.ts @@ -25,5 +25,6 @@ export default { '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`) + 'poll_required_majority.absolute_majority': _(`Absolute`), + 'poll_required_majority.simple_majority': _(`Simple`) }; 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 d0290936d0..03dcaf22d6 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 @@ -83,6 +83,9 @@ {{ 'absolute_majority' | translateKey: 'poll_required_majority' }} + + {{ 'simple_majority' | translateKey: 'poll_required_majority' }} + @if (form.get('required_majority').getRawValue() === 'absolute_majority') { {{ 'absolute_majority' | translateKey: 'poll_required_majority' }} + + {{ 'simple_majority' | translateKey: 'poll_required_majority' }} + @if (form.get('required_majority').getRawValue() === 'absolute_majority') { { protected getFilterDefinitions(): OsFilter[] { return [ + { + property: `isPresent`, + label: _(`Is present`), + options: [ + { label: _(`Is present`), condition: true }, + { label: _(`Is not present`), condition: [false, null] } + ] + }, { property: `hasVoted`, label: _(`Voted`), diff --git a/client/src/app/site/pages/meetings/pages/polls/components/poll-entitled-users/poll-entitled-user.component.ts b/client/src/app/site/pages/meetings/pages/polls/components/poll-entitled-users/poll-entitled-user.component.ts index 598c00ad5a..f9dd6d5d4d 100644 --- a/client/src/app/site/pages/meetings/pages/polls/components/poll-entitled-users/poll-entitled-user.component.ts +++ b/client/src/app/site/pages/meetings/pages/polls/components/poll-entitled-users/poll-entitled-user.component.ts @@ -10,6 +10,8 @@ import { MatTabsModule } from '@angular/material/tabs'; import { MatTooltipModule } from '@angular/material/tooltip'; import { Id } from '@app/domain/definitions/key-types'; import { Identifiable } from '@app/domain/interfaces'; +import { PollState } from '@app/domain/models/poll'; +import { MeetingUserRepositoryService } from '@app/gateways/repositories/meeting_user'; import { BaseComponent } from '@app/site/base/base.component'; import { EntitledUsersListFilterService } from '@app/site/pages/meetings/modules/poll/services/entitled-user-filter.service'; import { ViewPoll } from '@app/site/pages/meetings/pages/polls/view-models'; @@ -18,9 +20,10 @@ import { HeadBarModule } from '@app/ui/modules/head-bar'; import { ListModule } from '@app/ui/modules/list'; import { PipesModule } from '@app/ui/pipes'; import { TranslatePipe } from '@ngx-translate/core'; -import { combineLatest, map, Observable, startWith, switchMap, tap } from 'rxjs'; +import { combineLatest, map, Observable, of, startWith, switchMap, tap } from 'rxjs'; import { ViewMeetingUser } from '../../../../view-models/view-meeting-user'; +import { ViewPollEntitledUser } from '../../view-models/poll-entitled-user'; export interface EntitledUserData extends Identifiable { meetingUser: ViewMeetingUser; @@ -58,19 +61,53 @@ export class PollEntitledUserComponent extends BaseComponent { public filterProps = [`meetingUser.user.getFullName`]; public entitledUsers$: Observable = toObservable(this.poll).pipe( - switchMap(poll => combineLatest([poll.entitled_users$, poll.ballot_users$.pipe(startWith([]))])), - map(([users, voted]) => { + switchMap(poll => + poll.state === PollState.Finished + ? combineLatest([ + of(poll), + poll.entitled_users$, + poll.ballot_users$.pipe(startWith([])), + of([] as ViewMeetingUser[]) + ]) + : combineLatest([ + of(poll), + of([] as ViewPollEntitledUser[]), + poll.ballot_users$.pipe(startWith([])), + this.userRepo.getViewModelListObservable().pipe( + map(users => { + return users.filter(mu => { + const countable = mu.user.isVoteCountable; + const inVoteGroup = poll.entitled_group_ids.intersect(mu.group_ids).length; + + return countable && inVoteGroup; + }); + }) + ) + ]) + ), + map(([poll, entitledUsersAtStop, voted, entitledUsers]) => { const votedSet = new Set(); for (const user of voted) { votedSet.add(user.represented_meeting_user_id); } - return users.map( + if (poll.state === PollState.Finished) { + return entitledUsersAtStop.map( + eUser => + ({ + meetingUser: eUser.meeting_user, + isPresent: eUser.present, + hasVoted: votedSet.has(eUser.meeting_user_id) + }) as EntitledUserData + ); + } + + return entitledUsers.map( eUser => ({ - meetingUser: eUser.meeting_user, - isPresent: false, // TODO: Implement or remove - hasVoted: votedSet.has(eUser.meeting_user_id) + meetingUser: eUser, + isPresent: eUser.user.is_present, + hasVoted: votedSet.has(eUser.id) }) as EntitledUserData ); }), @@ -80,4 +117,5 @@ export class PollEntitledUserComponent extends BaseComponent { public totalCount = signal(0); public filter = inject(EntitledUsersListFilterService); + public userRepo = inject(MeetingUserRepositoryService); } From 84cb075820c0a4204bd448b5e26afe96e2562bed Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Wed, 23 Sep 2026 14:29:27 +0200 Subject: [PATCH 05/26] Rename poll titles --- .../topics/modules/topic-poll/services/topic-poll.service.ts | 2 +- .../modules/assignment-poll/services/assignment-poll.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 69eae86eda..e61161f0fa 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 @@ -37,7 +37,7 @@ export class TopicPollService extends PollService { public getDefaultPollData(contentObject?: Topic): Partial { const poll: Partial = { - title: this.translate.instant(`Poll`), + title: this.translate.instant(`Vote`), entitled_group_ids: Object.values(this.defaultGroupIds() ?? []), visibility: this.isElectronicVotingEnabled ? this.defaultPollType() : PollVisibility.Manually, live_voting_enabled: this.defaultEnableLiveVote(), 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 5148daf72d..0e9aedbb0f 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 @@ -48,7 +48,7 @@ export class AssignmentPollService extends PollService { public getDefaultPollData(contentObject?: Assignment): Partial { const poll: Partial = { - title: this.translate.instant(`Poll`), + title: this.translate.instant(`Ballot`), entitled_group_ids: Object.values(this.defaultGroupIds() ?? []), visibility: this.isElectronicVotingEnabled ? this.defaultPollType() : PollVisibility.Manually, live_voting_enabled: this.defaultEnableLiveVote(), From 9adf7139c59ed20aba9214eea5116ebfbcd0a683 Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Wed, 23 Sep 2026 15:33:06 +0200 Subject: [PATCH 06/26] Fix poll meta not updating --- .../poll-meta/poll-meta.component.html | 56 +++++++++---------- .../poll-meta/poll-meta.component.ts | 31 +++++----- 2 files changed, 44 insertions(+), 43 deletions(-) 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 8da3755793..3bc16aae68 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 @@ -65,102 +65,102 @@ } - @if (config().strike_out !== undefined || config().allow_abstain !== undefined) { + @if (config.value().strike_out !== undefined || config.value().allow_abstain !== undefined) {
{{ 'Voting method' | translate }}: - @if (config().strike_out === true) { + @if (config.value().strike_out === true) { @if (isPersonPoll()) { {{ 'No per candidate' | translate }} } @else { {{ 'No per option' | translate }} } - } @else if (config().strike_out === false) { + } @else if (config.value().strike_out === false) { @if (isPersonPoll()) { {{ 'Yes per candidate' | translate }} } @else { {{ 'Yes per option' | translate }} } - } @else if (config().allow_abstain === true) { + } @else if (config.value().allow_abstain === true) { {{ 'Yes/No/Abstain' | translate }} - } @else if (config().allow_abstain === false) { + } @else if (config.value().allow_abstain === false) { {{ 'Yes/No' | translate }} }
} - @if (config().COLLECTION === `poll_config_selection`) { - @if (config().min_options_amount > 1 || config().max_options_amount > 1) { - @if (config().min_options_amount === config().max_options_amount) { + @if (config.value().COLLECTION === `poll_config_selection`) { + @if (config.value().min_options_amount > 1 || config.value().max_options_amount > 1) { + @if (config.value().min_options_amount === config.value().max_options_amount) {
{{ 'Votes on different options' | translate }}: - {{ config().min_options_amount }} + {{ config.value().min_options_amount }}
} @else { - @if (config().min_options_amount > 1) { + @if (config.value().min_options_amount > 1) {
{{ 'Minimum amount of options selected' | translate }}: - {{ config().min_options_amount }} + {{ config.value().min_options_amount }}
} - @if (config().max_options_amount > 1) { + @if (config.value().max_options_amount > 1) {
{{ 'Maximum amount options selected' | translate }}: - {{ config().max_options_amount }} + {{ config.value().max_options_amount }}
} } } } @else { - @if (config().min_vote_sum > 1 || config().max_vote_sum > 1) { - @if (config().min_vote_sum === config().max_vote_sum) { + @if (config.value().min_vote_sum > 1 || config.value().max_vote_sum > 1) { + @if (config.value().min_vote_sum === config.value().max_vote_sum) {
{{ 'Votes' | translate }}: - {{ config().min_vote_sum }} + {{ config.value().min_vote_sum }}
} @else { - @if (config().min_vote_sum > 1) { + @if (config.value().min_vote_sum > 1) {
{{ 'Minimum amount of votes' | translate }}: - {{ config().min_vote_sum }} + {{ config.value().min_vote_sum }}
} - @if (config().max_vote_sum > 1) { + @if (config.value().max_vote_sum > 1) {
{{ 'Maximum amount of votes' | translate }}: - {{ config().max_vote_sum }} + {{ config.value().max_vote_sum }}
} } } - @if (config().max_yes_amount >= 1 && config().max_yes_amount < config().max_vote_sum) { + @if (config.value().max_yes_amount >= 1 && config.value().max_yes_amount < config.value().max_vote_sum) {
{{ 'Maximum amount of yes votes' | translate }}: - {{ config().max_yes_amount }} + {{ config.value().max_yes_amount }}
} } - @if (config().max_votes_per_option > 1) { + @if (config.value().max_votes_per_option > 1) {
{{ 'Maximum amount of votes per option' | translate }}: - {{ config().max_votes_per_option }} + {{ config.value().max_votes_per_option }}
} - @if (config().onehundred_percent_base !== undefined) { + @if (config.value().onehundred_percent_base !== undefined) {
{{ '100% base' | translate }}: - {{ config().onehundred_percent_base | translateKey: 'poll_percent_base' }} + {{ config.value().onehundred_percent_base | translateKey: 'poll_percent_base' }}
} - @if (config().required_majority !== undefined && config().required_majority !== 'no_majority') { + @if (config.value().required_majority !== undefined && config.value().required_majority !== 'no_majority') {
{{ 'Required majority' | translate }}: - {{ config().required_majority | translateKey: 'poll_required_majority' }} + {{ config.value().required_majority | translateKey: 'poll_required_majority' }}
} diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-meta/poll-meta.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-meta/poll-meta.component.ts index c5619b9415..da26ca5513 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-meta/poll-meta.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-meta/poll-meta.component.ts @@ -1,5 +1,6 @@ import { NgTemplateOutlet } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; +import { rxResource } from '@angular/core/rxjs-interop'; import { PollConfigApproval } from '@app/domain/models/poll/poll-config-approval'; import { ViewPoll } from '@app/site/pages/meetings/pages/polls/view-models'; import { ViewMeetingUser } from '@app/site/pages/meetings/view-models/view-meeting-user'; @@ -25,36 +26,36 @@ import { ViewPollOption } from '../../../../pages/polls/view-models/poll-option' export class PollMetaComponent { public poll = input.required(); - public config = computed< - Partial< - ViewPollConfigApproval & - ViewPollConfigRatingApproval & - ViewPollConfigRatingScore & - ViewPollConfigSelection & - ViewPollConfigStvScottish - > - >(() => { - return this.poll().config || {}; + public config = rxResource< + ViewPollConfigApproval & + ViewPollConfigRatingApproval & + ViewPollConfigRatingScore & + ViewPollConfigSelection & + ViewPollConfigStvScottish, + ViewPoll + >({ + params: () => this.poll(), + stream: ({ params }) => params.config$ }); public hasGlobalOptionEnabled = computed(() => { - return this.config().allow_nota || this.config().min_options_amount === 0; + return this.config.value().allow_nota || this.config.value().min_options_amount === 0; }); public generalApprovalAllowed = computed(() => { - return this.config().allow_nota && this.config().strike_out; + return this.config.value().allow_nota && this.config.value().strike_out; }); public generalRejectionAllowed = computed(() => { - return this.config().allow_nota && !this.config().strike_out; + return this.config.value().allow_nota && !this.config.value().strike_out; }); public generalAbstainAllowed = computed(() => { - return this.config().min_options_amount === 0; + return this.config.value().min_options_amount === 0; }); public isListPoll = computed(() => { - return this.config().collection === PollConfigApproval.COLLECTION && !!this.poll().options?.length; + return this.config.value().collection === PollConfigApproval.COLLECTION && !!this.poll().options?.length; }); public isPersonPoll = computed(() => { From 9aa4eb7eef80c5230f14d22ae1c0c6c7ac0b2f34 Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Wed, 23 Sep 2026 15:33:29 +0200 Subject: [PATCH 07/26] Fix poll form field sideeffects --- .../poll-form/poll-form.component.ts | 9 +++++---- .../topic-poll-dialog.component.html | 2 +- .../topic-poll-dialog.component.ts | 18 +----------------- 3 files changed, 7 insertions(+), 22 deletions(-) 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 7f69b2744b..08098297e9 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 @@ -207,12 +207,13 @@ export class PollFormComponent extends BaseComponent { private updateData(): void { const data = this.data(); if (data && this.form) { - if (data.entitled_group_ids !== undefined) + if (data.entitled_group_ids !== undefined && !this.form['entitled_group_ids']().dirty()) this.form['entitled_group_ids']().value.set(data.entitled_group_ids); - if (data.live_voting_enabled !== undefined) + if (data.live_voting_enabled !== undefined && !this.form['live_voting_enabled']().dirty()) 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.title !== undefined && !this.form['title']().dirty()) this.form['title']().value.set(data.title); + if (data.visibility !== undefined && !this.form['visibility']().dirty()) + this.form['visibility']().value.set(data.visibility); if (data.options !== undefined && !data.options.some(option => option.content_object_id)) this.form['options']().value.set(data.options.map(option => option.text)); if (data.config?.method) { 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 7a35b2e5f9..d7e2da3991 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 @@ -4,7 +4,7 @@ [data]="pollData" [isEVotingEnabled]="isEVotingEnabled" [optionAmount]="options().length" - [optionEdit]="selectedTab() === 0" + [optionEdit]="true" > diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-dialog/topic-poll-dialog.component.ts b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-dialog/topic-poll-dialog.component.ts index 5b0a1bb06e..2bc52a40a2 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-dialog/topic-poll-dialog.component.ts +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-dialog/topic-poll-dialog.component.ts @@ -1,9 +1,8 @@ -import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatDialogModule } from '@angular/material/dialog'; import { MatTabsModule } from '@angular/material/tabs'; import { djb2hash } from '@app/infrastructure/utils'; -import { collectionFromFqid } from '@app/infrastructure/utils/transform-functions'; import { BasePollDialogComponent, PollMethodPayload, @@ -14,8 +13,6 @@ import { PollFormComponent } from '@app/site/pages/meetings/modules/poll/compone import { PollService } from '@app/site/pages/meetings/modules/poll/services/poll.service'; import { TranslatePipe } from '@ngx-translate/core'; -const TAB_METHOD_MAP = [`selection`, `approval`]; - @Component({ selector: `os-topic-poll-dialog`, templateUrl: `./topic-poll-dialog.component.html`, @@ -37,25 +34,12 @@ export class TopicPollDialogComponent extends BasePollDialogComponent { return this.pollService.isElectronicVotingEnabled; } - public selectedTab = signal(0); - public options = computed(() => { return this.pollForm().form.options().value(); }); private pollService = inject(PollService); - public constructor() { - super(); - - if (this.pollData?.config_id) { - const collection = collectionFromFqid(this.pollData?.config_id); - this.selectedTab.set(TAB_METHOD_MAP.indexOf(collection.replace(`poll_config_`, ``))); - } else if (this.pollData?.config?.method) { - this.selectedTab.set(TAB_METHOD_MAP.indexOf(this.pollData.config.method)); - } - } - public override methodPayload(): PollMethodPayload { return { method: this.pollForm().selectedMethod(), From cf3e8e320f8ed46cd915c4ece5ede4d0754b21d6 Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Wed, 23 Sep 2026 16:44:06 +0200 Subject: [PATCH 08/26] majority/undocumented update on language switch --- .../poll-parse-number.pipe.ts | 9 +- .../services/poll.service/poll.service.ts | 61 -------- .../motion-poll-detail-content.component.ts | 137 ------------------ 3 files changed, 6 insertions(+), 201 deletions(-) delete mode 100644 client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-detail-content/motion-poll-detail-content.component.ts diff --git a/client/src/app/site/pages/meetings/modules/poll/pipes/poll-parse-number/poll-parse-number.pipe.ts b/client/src/app/site/pages/meetings/modules/poll/pipes/poll-parse-number/poll-parse-number.pipe.ts index 06220166c0..e254e3bfaf 100644 --- a/client/src/app/site/pages/meetings/modules/poll/pipes/poll-parse-number/poll-parse-number.pipe.ts +++ b/client/src/app/site/pages/meetings/modules/poll/pipes/poll-parse-number/poll-parse-number.pipe.ts @@ -1,12 +1,15 @@ -import { Pipe, PipeTransform } from '@angular/core'; +import { inject, Pipe, PipeTransform } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; import { PollService } from '../../services/poll.service'; @Pipe({ - name: `pollParseNumber` + name: `pollParseNumber`, + pure: false }) export class PollParseNumberPipe implements PipeTransform { - public constructor(private pollService: PollService) {} + protected translate = inject(TranslateService); + private pollService = inject(PollService); public transform(value?: number): string { return this.pollService.parseNumber(value, 3); diff --git a/client/src/app/site/pages/meetings/modules/poll/services/poll.service/poll.service.ts b/client/src/app/site/pages/meetings/modules/poll/services/poll.service/poll.service.ts index b4e539dda6..5cdd7c9596 100644 --- a/client/src/app/site/pages/meetings/modules/poll/services/poll.service/poll.service.ts +++ b/client/src/app/site/pages/meetings/modules/poll/services/poll.service/poll.service.ts @@ -3,7 +3,6 @@ import { OptionData } from '@app/domain/models/poll/generic-poll'; import { BaseOnehundredPercentBase } from '@app/domain/models/poll/poll-config-types'; import { ABSTAIN_KEY, - CalculablePollKey, isPollTableData, NO_KEY, PollTableData, @@ -13,7 +12,6 @@ import { VotingResult, YES_KEY } from '@app/domain/models/poll/poll-constants'; -import { ChartData, ChartDate } from '@app/site/pages/meetings/modules/poll/components/chart/chart.component'; import { ViewPoll } from '@app/site/pages/meetings/pages/polls/view-models'; import { OrganizationSettingsService } from '@app/site/pages/organization/services/organization-settings.service'; import { ThemeService } from '@app/site/services/theme.service'; @@ -22,7 +20,6 @@ import { TranslateService } from '@ngx-translate/core'; import { ActiveMeetingService } from '../../../../services/active-meeting.service'; import { MeetingSettingsService } from '../../../../services/meeting-settings.service'; -const PollChartBarThickness = 20; const PERCENT_DECIMAL_PLACES = 3; @Service() @@ -248,64 +245,6 @@ export abstract class PollService { return base === `valid` || base === `cast` || base === `entitled` || base === `entitled_present`; } - public getSumTableKeys(_poll: ViewPoll): VotingResult[] { - return []; - /* - return [ - { - vote: `votesvalid`, - hide: poll.votesvalid === VOTE_UNDOCUMENTED, - showPercent: this.showPercentOfValidOrCast(poll) - }, - { - vote: `votesinvalid`, - hide: poll.votesinvalid === VOTE_UNDOCUMENTED || poll.type !== PollType.Analog, - showPercent: poll.config?.onehundred_percent_base === PollPercentBase.Cast - }, - { - vote: `votescast`, - hide: poll.votescast === VOTE_UNDOCUMENTED || poll.type !== PollType.Analog, - showPercent: poll.config?.onehundred_percent_base === PollPercentBase.Cast - } - ]; - */ - } - - public generateChartData(poll: ViewPoll): ChartData { - let fields = this.getPollDataFields(poll); - if (poll.config?.onehundred_percent_base === `yes_no` || !poll.config?.allow_abstain) { - fields = fields.filter(key => key === YES_KEY || key === NO_KEY); - } - - const data: ChartData = fields - .map( - key => - ({ - data: this.getResultFromPoll(poll, key), // 0: option, 1: global_option - label: key.toUpperCase(), - backgroundColor: this.themeService.getPollColor(key), - hoverBackgroundColor: this.themeService.getPollColor(key), - barThickness: PollChartBarThickness, - maxBarThickness: PollChartBarThickness - }) as ChartDate - ) - .filter(chartDate => !!chartDate.data[0] || !!chartDate.data[1]); - - return data; - } - - /** - * Extracts yes-no-abstain such as valid, invalids and totals from Poll and PollData-Objects - */ - protected getResultFromPoll(_poll: ViewPoll, _key: CalculablePollKey): (number | undefined)[] { - // return (poll ? [...poll.options, poll.global_option] : []).map(option => (option ? option[key] : undefined)); - return []; - } - - protected getPollDataFields(_poll: ViewPoll): CalculablePollKey[] { - throw new Error(`Method not implemented`); - } - public isVoteDocumented(vote: number): boolean { return vote !== null && vote !== undefined && vote !== VOTE_UNDOCUMENTED; } diff --git a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-detail-content/motion-poll-detail-content.component.ts b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-detail-content/motion-poll-detail-content.component.ts deleted file mode 100644 index d5cbce0ad3..0000000000 --- a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-detail-content/motion-poll-detail-content.component.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { Permission } from '@app/domain/definitions/permission'; -import { VOTE_UNDOCUMENTED } from '@app/domain/models/poll'; -import { PollData } from '@app/domain/models/poll/generic-poll'; -import { PollState, PollTableData } from '@app/domain/models/poll/poll-constants'; -import { ChartData } from '@app/site/pages/meetings/modules/poll/components/chart/chart.component'; -import { PollService } from '@app/site/pages/meetings/modules/poll/services/poll.service'; -import { OperatorService } from '@app/site/services/operator.service'; -import { ThemeService } from '@app/site/services/theme.service'; -import { BaseUiComponent } from '@app/ui/base/base-ui-component'; -import { combineLatestWith, map } from 'rxjs'; - -import { MotionPollService } from '../../services/motion-poll.service/motion-poll.service'; - -const CHART_DATA_SUBSCRIPTION_NAME = `chart_data_subscription`; - -@Component({ - selector: `os-motion-poll-detail-content`, - templateUrl: `./motion-poll-detail-content.component.html`, - styleUrls: [`./motion-poll-detail-content.component.scss`], - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: false -}) -export class MotionPollDetailContentComponent extends BaseUiComponent implements OnDestroy, OnInit { - private _poll!: PollData | null; - - public get chartData(): ChartData { - return this._chartData!; - } - - public get tableData(): PollTableData[] { - return this._tableData; - } - - @Input() - public set poll(pollData: PollData | null) { - this._poll = pollData; - if (this._poll) { - this.setupTableData(); - this.cd.markForCheck(); - } - } - - public get poll(): PollData | null { - return this._poll; - } - - @Input() - public title = ``; - - @Input() - public shouldShowEntitled = false; - - @Input() - public iconSize: `medium` | `large` | `gigantic` = `large`; - - @Input() - public inSlide = false; - - private get state(): PollState | null { - return this.poll?.state || null; - } - - public get hasResults(): boolean { - return this.isFinished || this.isPublished; - } - - public get isStarted(): boolean { - return this.state === PollState.Started; - } - - public get isFinished(): boolean { - return this.state === PollState.Finished; - } - - public get isPublished(): boolean { - return this.state === PollState.Published; - } - - public get canSeeResults(): boolean { - return this.operator.hasPerms(Permission.motionCanManagePolls) || this.isPublished; - } - - public get shouldShowChart(): boolean { - return this.pollService.shouldShowChart(this.poll); - } - - public get motionPollService(): PollService { - return this.pollService; - } - - public isPercentBaseEntitled = false; - public isPercentBaseEntitledPresent = false; - - private _tableData: PollTableData[] = []; - private _chartData: ChartData | null = null; - - public constructor( - private pollService: MotionPollService, - private operator: OperatorService, - private cd: ChangeDetectorRef, - private themeService: ThemeService - ) { - super(); - } - - public ngOnInit(): void { - this.subscriptions.push( - this._poll.options$.subscribe(() => { - this.setupTableData(); - }) - ); - } - - private setupTableData(): void { - this.updateSubscription( - CHART_DATA_SUBSCRIPTION_NAME, - this.pollService - .generateTableDataAsObservable(this.poll!) - .pipe( - combineLatestWith(this.themeService.currentGeneralColorsSubject), - map(([tableData, _]) => tableData) - ) - .subscribe(tableData => { - this._tableData = tableData.filter(result => result.value[0].amount !== VOTE_UNDOCUMENTED); - this.setChartData(); - this.cd.markForCheck(); - }) - ); - } - - private setChartData(): void { - this._chartData = this.pollService - .generateChartData(this.poll!) - .filter(result => result.data[0] !== VOTE_UNDOCUMENTED); - } -} From a08114c2f43d02379b797b2beae070112b5c1328 Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Wed, 23 Sep 2026 17:07:25 +0200 Subject: [PATCH 09/26] Fix analog poll update --- .../poll-repository.service.ts | 47 +++++++------------ 1 file changed, 16 insertions(+), 31 deletions(-) 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 aff7fa7a8f..dac3225ab3 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 @@ -47,36 +47,6 @@ export class PollRepositoryService extends BaseMeetingRelatedRepository { - if (payload.visibility === PollVisibility.Manually) { - return this.updateAnalogPoll(poll, payload); - } - - return this.updateElectronicPoll(poll, payload); - } - - private async createAnalogPoll(payload: any): Promise { - delete payload.live_voting_enabled; - - return this.voteApi.create(payload); - } - - private async createElectronicPoll(payload: PollCreatePayload): Promise { - if (payload.visibility !== PollVisibility.Named && payload.visibility !== PollVisibility.Open) { - delete payload.live_voting_enabled; - } - - return this.voteApi.create(payload); - } - - private async updateAnalogPoll(poll: ViewPoll, payload: PollUpdatePayload): Promise { - if (poll.config.method === payload.method) { - delete payload[`method`]; - } - - return this.voteApi.update(poll.id, payload); - } - - private async updateElectronicPoll(poll: ViewPoll, payload: PollUpdatePayload): Promise { if (poll.state !== PollState.Created) { const methodConfig: { display_chart?: string; @@ -89,7 +59,8 @@ export class PollRepositoryService extends BaseMeetingRelatedRepository { + delete payload.live_voting_enabled; + + return this.voteApi.create(payload); + } + + private async createElectronicPoll(payload: PollCreatePayload): Promise { + if (payload.visibility !== PollVisibility.Named && payload.visibility !== PollVisibility.Open) { + delete payload.live_voting_enabled; + } + + return this.voteApi.create(payload); + } + public async delete(poll: Identifiable): Promise { return this.voteApi.deletePoll(poll.id); } From a69ac8e282074406c2334be031d2a05e5330c38f Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Thu, 24 Sep 2026 10:28:08 +0200 Subject: [PATCH 10/26] Fix analog poll result not updated --- .../poll/components/poll-result-base.component.ts | 5 +---- .../poll-result-rating-score.component.html | 2 +- .../poll-result-rating-score.component.ts | 9 +++++++-- .../components/poll-result/poll-result.component.html | 8 ++++---- .../poll/components/poll-result/poll-result.component.ts | 6 ++++++ 5 files changed, 19 insertions(+), 11 deletions(-) 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 1ad1b9161f..d085f280fb 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 @@ -9,6 +9,7 @@ export const PERCENT_DECIMAL_PLACES = 3; @Directive() export abstract class PollResultBaseComponent { public poll = input.required(); + public config = input.required(); public results = computed(() => { if (!this.poll().result) { @@ -18,10 +19,6 @@ export abstract class PollResultBaseComponent { return this.poll()?.config?.parsedResult() || {}; }); - public config = computed(() => { - return this.poll().config; - }); - /** * Contains the poll options sorted by weight */ diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-result-rating-score/poll-result-rating-score.component.html b/client/src/app/site/pages/meetings/modules/poll/components/poll-result-rating-score/poll-result-rating-score.component.html index 4f81f6a1a4..80b39bd728 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-result-rating-score/poll-result-rating-score.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-result-rating-score/poll-result-rating-score.component.html @@ -1 +1 @@ - + diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-result-rating-score/poll-result-rating-score.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-result-rating-score/poll-result-rating-score.component.ts index 2c824705b1..f44d627d13 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-result-rating-score/poll-result-rating-score.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-result-rating-score/poll-result-rating-score.component.ts @@ -1,4 +1,5 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed } from '@angular/core'; +import { ViewPollConfigSelection } from '@app/site/pages/meetings/pages/polls'; import { ViewPollConfigRatingScore } from '../../../../pages/polls/view-models/poll-config-rating-score'; import { RatingScorePollResult } from '../../../../pages/polls/view-models/poll-result-rating-score'; @@ -15,4 +16,8 @@ import { PollResultSelectionComponent } from '../poll-result-selection/poll-resu export class PollResultRatingScoreComponent extends PollResultBaseComponent< ViewPollConfigRatingScore, RatingScorePollResult -> {} +> { + public configSelection = computed(() => { + return this.config() as unknown as 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 d334e2b743..ed9d0d60dc 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 ('approval') { - + } @case ('rating_approval') { - + } @case ('rating_score') { - + } @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 ab886423fe..7ab6e3ee3c 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,4 +1,5 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; +import { rxResource } from '@angular/core/rxjs-interop'; import { ViewPoll } from '@app/site/pages/meetings/pages/polls/view-models'; import { PollResultApprovalComponent } from '../poll-result-approval/poll-result-approval.component'; @@ -21,6 +22,11 @@ import { PollResultSelectionComponent } from '../poll-result-selection/poll-resu export class PollResultComponent { public poll = input.required(); + public config = rxResource({ + params: () => this.poll(), + stream: ({ params }) => params.config$ + }); + public configType = computed(() => { return this.poll().config?.method ?? `none`; }); From 6ad87a2302332d6050044f7eafde0bce58f1ec49 Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Mon, 28 Sep 2026 15:21:21 +0200 Subject: [PATCH 11/26] Allow empty support for selection/score polls --- client/src/app/gateways/vote-api.service.ts | 7 ++-- .../poll/base/base-poll-dialog.component.ts | 38 +++++++++++++------ .../poll-config-form-base.component.ts | 4 ++ .../poll-edit-result.component.html | 2 +- .../poll-edit-result.component.ts | 20 +++++----- .../poll-form-approval.component.ts | 7 ++-- .../poll-form-rating-approval.component.ts | 7 ++-- .../poll-form-rating-score.component.ts | 29 +++++++------- .../poll-form-selection.component.html | 32 +++++++--------- .../poll-form-selection.component.ts | 28 ++++++++------ .../poll-form/poll-form.component.ts | 2 +- .../poll-meta/poll-meta.component.ts | 4 +- .../poll-result-selection.component.ts | 4 +- .../poll-vote-rating-approval.component.html | 4 +- .../poll-vote-rating-score.component.html | 2 +- .../poll-vote-selection.component.html | 4 +- .../poll-vote-selection.component.ts | 2 +- .../modules/poll/services/poll-pdf.service.ts | 4 +- .../topic-poll-dialog.component.html | 6 +-- .../assignment-poll-dialog.component.html | 6 +-- .../motion-poll-dialog.component.html | 6 +-- .../poll-result-rating-approval.ts | 2 +- .../view-models/poll-result-rating-score.ts | 2 +- .../view-models/poll-result-selection.ts | 4 +- 24 files changed, 123 insertions(+), 103 deletions(-) diff --git a/client/src/app/gateways/vote-api.service.ts b/client/src/app/gateways/vote-api.service.ts index 1b8e8a574e..87e5421b8c 100644 --- a/client/src/app/gateways/vote-api.service.ts +++ b/client/src/app/gateways/vote-api.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { inject, Service } from '@angular/core'; import { Id } from '../domain/definitions/key-types'; import { PollVisibility } from '../domain/models/poll/poll-constants'; @@ -20,13 +20,14 @@ export interface PollUpdatePayload { option_type?: 'text' | 'meeting_user'; result?: unknown; allow_vote_split: boolean; + allow_empty?: boolean; } -@Injectable({ providedIn: 'root' }) +@Service() export class VoteApiService { private readonly BASE = '/system/vote'; - public constructor(private http: HttpService) {} + private http = inject(HttpService); public create(payload: PollCreatePayload): Promise { return this.http.post(`${this.BASE}/poll`, payload); 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 33e9f15774..b7cddf35eb 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 @@ -57,20 +57,46 @@ export abstract class BasePollDialogComponent extends BaseUiComponent { public dialogRef = inject(MatDialogRef); public pollData: ViewPoll = inject(MAT_DIALOG_DATA); + public currentPollData: ViewPoll = this.pollData; + public constructor() { super(); this.addKeyListener(); } + public toggleAnalogPollForm(): void { + const payload = this.getPayload(); + if (payload.result && !(payload.result instanceof String)) { + payload.result = JSON.stringify(payload.result); + } + this.currentPollData = Object.assign(this.currentPollData, payload); + this.analogPollFormOpen.set(!this.analogPollFormOpen()); + } + /** * Submits the values from dialog. */ public submitPoll(): void { + this.dialogRef.close(this.getPayload()); + } + + /** + * Returns the poll method related part of the payload + */ + public abstract methodPayload(): PollMethodPayload; + + /** + * Returns the poll options related part of the payload + */ + public abstract optionsPayload(): PollOptionsPayload; + + protected getPayload(): PollUpdatePayload { const formValues = this.pollForm().getValues(); const visibility: PollVisibility = formValues?.visibility; const payload: PollUpdatePayload = { title: formValues?.title, + allow_empty: formValues?.allow_empty, ...this.methodPayload(), ...this.optionsPayload(), visibility, @@ -87,19 +113,9 @@ export abstract class BasePollDialogComponent extends BaseUiComponent { } } - this.dialogRef.close(payload); + return payload; } - /** - * Returns the poll method related part of the payload - */ - public abstract methodPayload(): PollMethodPayload; - - /** - * Returns the poll options related part of the payload - */ - public abstract optionsPayload(): PollOptionsPayload; - private addKeyListener(): void { if (!this.dialogRef) { return; 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 7ddfff1b4c..21bfbbb385 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 @@ -29,6 +29,10 @@ export abstract class PollFormBaseComponent { protected abstract getPatchedFormData(data: Partial): Record; public abstract getSerialzedForm(): Record; + public getSerialzedAdditionalData(): Record { + return {}; + } + private onDataUpdated(): void { if (!this.data() || !this.form) { return; 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 12c5e575b6..6c898d956a 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 @@ -82,7 +82,7 @@

{{ option.title }}

Global abstain - +
} diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-edit-result/poll-edit-result.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-edit-result/poll-edit-result.component.ts index e808af3112..3c345a8731 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-edit-result/poll-edit-result.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-edit-result/poll-edit-result.component.ts @@ -1,5 +1,5 @@ import { NgTemplateOutlet } from '@angular/common'; -import { ChangeDetectionStrategy, Component, computed, input, OnInit, signal } from '@angular/core'; +import { Component, computed, input, OnInit, signal } from '@angular/core'; import { applyEach, disabled, form, FormField, schema } from '@angular/forms/signals'; import { MatButtonModule } from '@angular/material/button'; import { MatCheckboxModule } from '@angular/material/checkbox'; @@ -39,7 +39,7 @@ interface OptionFormEntry { interface PollEditResultModel { options: OptionFormEntry[]; - abstain: number; + empty: number; nota: number; invalid: number; total_ballots: number; @@ -59,8 +59,7 @@ interface PollEditResultModel { FormField, TranslatePipe, NgTemplateOutlet - ], - changeDetection: ChangeDetectionStrategy.OnPush + ] }) export class PollEditResultComponent implements OnInit { public readonly pollData = input.required>(); @@ -72,7 +71,7 @@ export class PollEditResultComponent implements OnInit { private readonly model = signal({ options: [], - abstain: 0, + empty: 0, nota: 0, invalid: 0, total_ballots: 0 @@ -102,8 +101,7 @@ export class PollEditResultComponent implements OnInit { }); public showAbstain = computed(() => { - const cfg = this.pollConfigDataLax(); - return cfg.min_options_amount === 0 && (cfg.min_vote_sum === 0 || cfg.min_vote_sum === undefined); + return this.pollData().allow_empty; }); public showNota = computed(() => { @@ -154,7 +152,7 @@ export class PollEditResultComponent implements OnInit { this.model.set({ options: builtOptions, - abstain: this.showAbstain() ? (result[`abstain`] ?? null) : null, + empty: this.showAbstain() ? (result[`empty`] ?? null) : null, nota: result[`nota`] ?? null, invalid: result[`invalid`] ?? null, total_ballots: result[`total_ballots`] ?? null @@ -172,7 +170,7 @@ export class PollEditResultComponent implements OnInit { .total_ballots() .value.set( data.invalid + - data.abstain + + data.empty + data.nota + data.options.reduce((p, c) => p + c.value.yes + c.value.no + c.value.abstain, 0) ); @@ -181,7 +179,7 @@ export class PollEditResultComponent implements OnInit { .total_ballots() .value.set( data.invalid + - data.abstain + + data.empty + data.nota + data.options.reduce((p, c) => Math.max(c.value.yes + c.value.no + c.value.abstain, p), 0) ); @@ -223,7 +221,7 @@ export class PollEditResultComponent implements OnInit { : serializedOptions), ...(m.invalid !== null ? { invalid: m.invalid } : {}), ...(m.total_ballots !== null ? { total_ballots: m.total_ballots } : {}), - ...(this.showAbstain() ? { abstain: m.abstain } : {}), + ...(this.showAbstain() ? { empty: m.empty } : {}), ...(this.showNota() ? { nota: m.nota } : {}) }; } 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 059fa64567..920ceae027 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, input } from '@angular/core'; +import { Component, input } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -26,8 +26,7 @@ export interface PollFormApproval { TranslateKeyPipe ], templateUrl: './poll-form-approval.component.html', - styleUrl: './poll-form-approval.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush + styleUrl: './poll-form-approval.component.scss' }) export class PollFormApprovalComponent extends PollFormBaseComponent { public hideMethod = input(false); @@ -38,7 +37,7 @@ export class PollFormApprovalComponent extends PollFormBaseComponent { [`valid`, _('All valid ballots')], [`cast`, _('All casted ballots')], [`entitled`, _('All entitled users')], - // [`entitled_present`, _('Present entitled users')], + [`entitled_present`, _('Present entitled users')], [`disabled`, _('Disabled (no percents)')] ]; 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 63d4fa010f..561c7a4281 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,4 @@ -import { ChangeDetectionStrategy, Component, effect, inject, input } from '@angular/core'; +import { 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'; @@ -34,8 +34,7 @@ export interface PollFormRatingApproval { TranslateKeyPipe ], templateUrl: './poll-form-rating-approval.component.html', - styleUrls: [`../poll-form/poll-form.component.scss`, `./poll-form-rating-approval.component.scss`], - changeDetection: ChangeDetectionStrategy.OnPush + styleUrls: [`../poll-form/poll-form.component.scss`, `./poll-form-rating-approval.component.scss`] }) export class PollFormRatingApprovalComponent extends PollFormBaseComponent { public validPercentBases: [RatingApprovalOnehundredPercentBase, string][] = [ @@ -44,7 +43,7 @@ export class PollFormRatingApprovalComponent extends PollFormBaseComponent { [`valid`, _('All valid ballots')], [`cast`, _('All casted ballots')], [`entitled`, _('All entitled users')], - // [`entitled_present`, _('Present entitled users')], + [`entitled_present`, _('Present entitled users')], [`disabled`, _('Disabled (no percents)')] ]; 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 cffe8b5f4e..dd37ea68bc 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,4 @@ -import { ChangeDetectionStrategy, Component, effect, input } from '@angular/core'; +import { 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'; @@ -33,8 +33,7 @@ export interface PollFormRatingScore { TranslateKeyPipe ], templateUrl: './poll-form-rating-score.component.html', - styleUrls: [`../poll-form/poll-form.component.scss`, `./poll-form-rating-score.component.scss`], - changeDetection: ChangeDetectionStrategy.OnPush + styleUrls: [`../poll-form/poll-form.component.scss`, `./poll-form-rating-score.component.scss`] }) export class PollFormRatingScoreComponent extends PollFormBaseComponent { public validPercentBases: [RatingScoreOnehundredPercentBase, string][] = [ @@ -42,24 +41,31 @@ export class PollFormRatingScoreComponent extends PollFormBaseComponent { [`valid`, _('All valid ballots')], [`cast`, _('All casted ballots')], [`entitled`, _('All entitled users')], - // [`entitled_present`, _('Present entitled users')], + [`entitled_present`, _('Present entitled users')], [`disabled`, _('Disabled (no percents)')] ]; public optionAmount = input(null); public getSerialzedForm(): Record { + const data = { + ...this.form.value + }; + delete data[`allow_empty`]; + + return data; + } + + public override getSerialzedAdditionalData(): Record { return { - ...this.form.value, - min_options_amount: this.form.value[`allow_general_abstain`] ? 0 : this.form.value[`min_options_amount`], - min_vote_sum: this.form.value[`allow_general_abstain`] ? 0 : this.form.value[`min_vote_sum`] + allow_empty: this.form.value[`allow_empty`] }; } protected initForm(): void { this.form = this.fb.group({ onehundred_percent_base: [`valid`], - allow_general_abstain: [false], + allow_empty: [false], max_votes_per_option: [null], max_options_amount: [1, [Validators.required, Validators.min(1)]], min_options_amount: [ @@ -97,16 +103,13 @@ export class PollFormRatingScoreComponent extends PollFormBaseComponent { `min_options_amount`, `max_vote_sum`, `min_vote_sum`, - `required_majority` + `required_majority`, + `allow_empty` ]) { if (data && data[field] !== undefined) patch[field] = data[field]; else if (data && data.config[field] !== undefined) patch[field] = data.config[field]; } - if (patch[`min_options_amount`] !== undefined && patch[`min_vote_sum`] !== undefined) { - patch[`allow_general_abstain`] = +patch[`min_options_amount`] === 0 && +patch[`min_vote_sum`] === 0; - } - return patch; } 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 69c3ccf311..7d5ac22951 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 @@ -14,22 +14,18 @@
@if (!pollStarted()) {
- @if (!form.get('allow_general_abstain').value) { - - {{ '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 { -
- } + + {{ '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 }} + + } + {{ 'Max amount of votes' | translate }} @@ -44,7 +40,7 @@
- + {{ 'General abstain' | translate }} @@ -66,7 +62,7 @@ @if ( option[0] !== 'no_general' || form.get('allow_nota').getRawValue() || - form.get('allow_general_abstain').getRawValue() || + form.get('allow_empty').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 30fc3e1906..3c23857349 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,5 +1,5 @@ import { AsyncPipe } from '@angular/common'; -import { ChangeDetectionStrategy, Component, effect, input } from '@angular/core'; +import { 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'; @@ -33,8 +33,7 @@ export interface PollFormSelection { AsyncPipe ], templateUrl: './poll-form-selection.component.html', - styleUrls: [`../poll-form/poll-form.component.scss`, './poll-form-selection.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + styleUrls: [`../poll-form/poll-form.component.scss`, './poll-form-selection.component.scss'] }) export class PollFormSelectionComponent extends PollFormBaseComponent { public validPercentBases: [SelectionOnehundredPercentBase, string][] = [ @@ -42,7 +41,7 @@ export class PollFormSelectionComponent extends PollFormBaseComponent { [`valid`, _('All valid ballots')], [`cast`, _('All casted ballots')], [`entitled`, _('All entitled users')], - // [`entitled_present`, _('Present entitled users')], + [`entitled_present`, _('Present entitled users')], [`disabled`, _('Disabled (no percents)')] ]; @@ -50,9 +49,17 @@ export class PollFormSelectionComponent extends PollFormBaseComponent { public optionAmount = input(null); public getSerialzedForm(): Record { + const data = { + ...this.form.value + }; + delete data[`allow_empty`]; + + return data; + } + + public override getSerialzedAdditionalData(): Record { return { - ...this.form.value, - min_options_amount: this.form.value[`allow_general_abstain`] ? 0 : this.form.value[`min_options_amount`] + allow_empty: this.form.value[`allow_empty`] }; } @@ -61,7 +68,7 @@ export class PollFormSelectionComponent extends PollFormBaseComponent { onehundred_percent_base: [`valid`], strike_out: [false], allow_nota: [false], - allow_general_abstain: [false], + allow_empty: [false], max_options_amount: [1, [Validators.required, Validators.min(1)]], min_options_amount: [1, [Validators.required, Validators.min(1), this.minOptionsAmountValidator()]], display_chart: [`table`], @@ -86,7 +93,8 @@ export class PollFormSelectionComponent extends PollFormBaseComponent { `max_options_amount`, `min_options_amount`, `display_chart`, - `required_majority` + `required_majority`, + `allow_empty` ]) { if (data && data[field] !== undefined) patch[field] = data[field]; else if (data && data.config[field] !== undefined) patch[field] = data.config[field]; @@ -96,10 +104,6 @@ export class PollFormSelectionComponent extends PollFormBaseComponent { patch[`onehundred_percent_base`] = `valid`; } - if (patch[`min_options_amount`] !== undefined) { - patch[`allow_general_abstain`] = +patch[`min_options_amount`] === 0; - } - return patch; } 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 08098297e9..0961d6167c 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 @@ -177,7 +177,7 @@ export class PollFormComponent extends BaseComponent { } public getValues(): Partial<{ [place in keyof ViewPoll]: any }> { - return { ...this.data, ...this.serializeForm() }; + return { ...this.data, ...this.serializeForm(), ...this.methodForm().getSerialzedAdditionalData() }; } public openVotingWarning(event: MouseEvent): void { diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-meta/poll-meta.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-meta/poll-meta.component.ts index da26ca5513..92f60de2b0 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-meta/poll-meta.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-meta/poll-meta.component.ts @@ -39,7 +39,7 @@ export class PollMetaComponent { }); public hasGlobalOptionEnabled = computed(() => { - return this.config.value().allow_nota || this.config.value().min_options_amount === 0; + return this.config.value().allow_nota || this.poll().allow_empty; }); public generalApprovalAllowed = computed(() => { @@ -51,7 +51,7 @@ export class PollMetaComponent { }); public generalAbstainAllowed = computed(() => { - return this.config.value().min_options_amount === 0; + return this.poll().allow_empty; }); public isListPoll = computed(() => { 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 2b6edb4a27..910386fedb 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 @@ -208,11 +208,11 @@ export class PollResultSelectionComponent extends PollResultBaseComponent< }); public generalAbstain = computed(() => { - if (this.config().min_options_amount !== 0) { + if (!this.poll().allow_empty) { return null; } - return +this.config().parsedResult()?.abstain || 0; + return +this.config().parsedResult()?.empty || 0; }); public generalAbstainPercent = computed(() => { diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-rating-approval/poll-vote-rating-approval.component.html b/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-rating-approval/poll-vote-rating-approval.component.html index c967886b5f..ead712bfb0 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-rating-approval/poll-vote-rating-approval.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-rating-approval/poll-vote-rating-approval.component.html @@ -98,7 +98,7 @@ }
-@if (config()?.min_vote_sum === 0) { +@if (poll().allow_empty) {

{{ 'OR' | translate }}
@@ -106,7 +106,7 @@
- @if (config().min_vote_sum === 0) { + @if (poll().allow_empty) {
{{ 'General abstain' | translate }} diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-rating-score/poll-vote-rating-score.component.html b/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-rating-score/poll-vote-rating-score.component.html index 51a89a85f9..e36814ce44 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-rating-score/poll-vote-rating-score.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-rating-score/poll-vote-rating-score.component.html @@ -92,7 +92,7 @@ }
-@if (config()?.min_vote_sum === 0 && config()?.min_options_amount === 0) { +@if (poll().allow_empty) {

{{ 'OR' | translate }}
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 9d5369cee2..71de2f8d3b 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 @@ -83,7 +83,7 @@ }
-@if (config()?.min_vote_sum === 0 || config()?.allow_nota) { +@if (config()?.allow_nota || poll().allow_empty) {

{{ 'OR' | translate }}
@@ -110,7 +110,7 @@
} - @if (config().min_vote_sum === 0) { + @if (poll().allow_empty) {
{{ 'General abstain' | translate }} diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-selection/poll-vote-selection.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-selection/poll-vote-selection.component.ts index c33d581f4e..05cf12e456 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-selection/poll-vote-selection.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-selection/poll-vote-selection.component.ts @@ -100,7 +100,7 @@ export class PollVoteSelectionComponent extends PollVoteBaseComponent
} @@ -36,11 +36,11 @@
@if (isAnalogPoll()) { @if (analogPollFormOpen()) { - } @else { - } diff --git a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html index 313b073aee..d8f11655a8 100644 --- a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html +++ b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html @@ -14,7 +14,7 @@ [configType]="pollForm().selectedMethod()" [options]="analogPollOptions()" [pollConfigData]="pollForm().methodConfig()" - [pollData]="pollData" + [pollData]="currentPollData" />
} @@ -36,11 +36,11 @@
@if (isAnalogPoll()) { @if (analogPollFormOpen()) { - } @else { - } diff --git a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.html b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.html index 9ef0e48e7d..20ec623407 100644 --- a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.html +++ b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.html @@ -11,7 +11,7 @@ configType="approval" [options]="analogPollOptions()" [pollConfigData]="approvalFormValue" - [pollData]="pollData" + [pollData]="currentPollData" />
} @@ -32,11 +32,11 @@
@if (isAnalogPoll()) { @if (analogPollFormOpen()) { - } @else { - } diff --git a/client/src/app/site/pages/meetings/pages/polls/view-models/poll-result-rating-approval.ts b/client/src/app/site/pages/meetings/pages/polls/view-models/poll-result-rating-approval.ts index 4c7bb2fb54..1b367009a5 100644 --- a/client/src/app/site/pages/meetings/pages/polls/view-models/poll-result-rating-approval.ts +++ b/client/src/app/site/pages/meetings/pages/polls/view-models/poll-result-rating-approval.ts @@ -8,5 +8,5 @@ export class RatingApprovalPollResult extends BasePollResult { [key: number]: string; - public abstain?: string; + public empty?: string; } diff --git a/client/src/app/site/pages/meetings/pages/polls/view-models/poll-result-selection.ts b/client/src/app/site/pages/meetings/pages/polls/view-models/poll-result-selection.ts index ee4b4716d5..8f75b7a9aa 100644 --- a/client/src/app/site/pages/meetings/pages/polls/view-models/poll-result-selection.ts +++ b/client/src/app/site/pages/meetings/pages/polls/view-models/poll-result-selection.ts @@ -6,7 +6,7 @@ import { ViewPollConfigSelection } from './poll-config-selection'; export class SelectionPollResult extends BasePollResult { [key: number]: string; public nota?: string; - public abstain?: string; + public empty?: string; public constructor( public override config: ViewPollConfigSelection, @@ -15,7 +15,7 @@ export class SelectionPollResult extends BasePollResult Date: Tue, 29 Sep 2026 10:38:05 +0200 Subject: [PATCH 12/26] Entitled users present 100% base --- .../poll/components/poll-form/poll-form.component.ts | 7 ++++++- .../poll-result-approval/poll-result-approval.component.ts | 7 +++++-- .../poll-result-rating-approval.component.ts | 7 +++++-- .../poll-result-selection.component.ts | 7 +++++-- .../site/pages/meetings/pages/polls/polls.subscription.ts | 4 ++++ .../pages/polls/view-models/poll-config-approval.ts | 2 +- .../pages/polls/view-models/poll-config-rating-approval.ts | 4 ++-- .../pages/polls/view-models/poll-config-rating-score.ts | 4 ++-- .../pages/polls/view-models/poll-config-selection.ts | 4 ++-- 9 files changed, 32 insertions(+), 14 deletions(-) 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 0961d6167c..d72287e07b 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 @@ -177,7 +177,12 @@ export class PollFormComponent extends BaseComponent { } public getValues(): Partial<{ [place in keyof ViewPoll]: any }> { - return { ...this.data, ...this.serializeForm(), ...this.methodForm().getSerialzedAdditionalData() }; + let additional = {}; + if (this.methodForm()) { + additional = this.methodForm().getSerialzedAdditionalData(); + } + + return { ...this.data, ...this.serializeForm(), ...additional }; } public openVotingWarning(event: MouseEvent): void { 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 c03bbab4cc..5ab0bc35c2 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 @@ -194,8 +194,11 @@ export class PollResultApprovalComponent extends PollResultBaseComponent(() => { - // TODO: Implement if available - return null; + if (this.config().onehundred_percent_base !== `entitled_present`) { + return null; + } + + return this.poll().entitled_users?.filter(u => u.present).length; }); public majorityReached = computed(() => { 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 aadddd7cfa..a586e52cae 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 @@ -138,8 +138,11 @@ export class PollResultRatingApprovalComponent extends PollResultBaseComponent< }); public presentEntitledUsers = computed(() => { - // TODO: Implement if available - return null; + if (this.config().onehundred_percent_base !== `entitled_present`) { + return null; + } + + return this.poll().entitled_users?.filter(u => u.present).length; }); public globalRequiredMajority = computed(() => { 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 910386fedb..8acc431661 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 @@ -189,8 +189,11 @@ export class PollResultSelectionComponent extends PollResultBaseComponent< }); public presentEntitledUsers = computed(() => { - // TODO: Implement if available - return null; + if (this.config().onehundred_percent_base !== `entitled_present`) { + return null; + } + + return this.poll().entitled_users?.filter(u => u.present).length; }); public requiredMajority = computed(() => { diff --git a/client/src/app/site/pages/meetings/pages/polls/polls.subscription.ts b/client/src/app/site/pages/meetings/pages/polls/polls.subscription.ts index b8650b0dc5..b6b5c9cb00 100644 --- a/client/src/app/site/pages/meetings/pages/polls/polls.subscription.ts +++ b/client/src/app/site/pages/meetings/pages/polls/polls.subscription.ts @@ -31,6 +31,10 @@ export const pollModelRequest: BaseSimplifiedModelRequest = { idField: `ballot_user_ids`, fieldset: [`acting_meeting_user_id`, `represented_meeting_user_id`] }, + { + idField: `entitled_user_ids`, + fieldset: [`present`] + }, { idField: `option_ids`, fieldset: FULL_FIELDSET, diff --git a/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-approval.ts b/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-approval.ts index 60c5b67ede..020dc7426d 100644 --- a/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-approval.ts +++ b/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-approval.ts @@ -31,7 +31,7 @@ export class ViewPollConfigApproval extends BasePollConfigViewModel u.present).length ?? null; } return null; 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 d0771ac4e7..d2d385fca6 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 @@ -35,7 +35,7 @@ export class ViewPollConfigRatingApproval extends BasePollConfigViewModel< case 'entitled': return this.poll.entitled_user_ids?.length; case 'entitled_present': - return null; + return this.poll?.entitled_users.filter(u => u.present).length ?? null; } return null; @@ -64,7 +64,7 @@ export class ViewPollConfigRatingApproval extends BasePollConfigViewModel< case 'entitled': return this.poll.entitled_user_ids?.length; case 'entitled_present': - return null; + return this.poll?.entitled_users.filter(u => u.present).length ?? null; } return null; diff --git a/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-rating-score.ts b/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-rating-score.ts index 2d6c257802..8e2a947984 100644 --- a/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-rating-score.ts +++ b/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-rating-score.ts @@ -28,9 +28,9 @@ export class ViewPollConfigRatingScore extends BasePollConfigViewModel u.present).length ?? null; } return null; diff --git a/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-selection.ts b/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-selection.ts index 1b35b48c09..ec0d92fb53 100644 --- a/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-selection.ts +++ b/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-selection.ts @@ -28,9 +28,9 @@ export class ViewPollConfigSelection extends BasePollConfigViewModel u.present).length ?? null; } return null; From 65bb2d0a7577160c75174549b9ad797d6f09c198 Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Tue, 29 Sep 2026 10:49:27 +0200 Subject: [PATCH 13/26] Fix edit rating approval with max yes votes disabled --- .../poll-form-rating-approval.component.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 561c7a4281..e772c32e65 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 @@ -55,14 +55,17 @@ export class PollFormRatingApprovalComponent extends PollFormBaseComponent { public maxYesVotesEnabled = this.meetingSettingsService.signal(`poll_enable_max_yes_votes`); protected initForm(): void { - this.form = this.fb.group({ + const fields = { onehundred_percent_base: [`valid`], 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()]], required_majority: [`no_majority`] - }); + }; + if (inject(MeetingSettingsService).instant(`poll_enable_max_yes_votes`)) { + fields[`max_yes_amount`] = [1, [Validators.required, Validators.min(1)]]; + } + this.form = this.fb.group(fields); effect(this.onOptionAmountUpdate.bind(this)); From fec8fba360eb63acc3d56470c8ea009586a75a96 Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Tue, 29 Sep 2026 11:04:49 +0200 Subject: [PATCH 14/26] Fix max yes votes displayed when disabled --- .../poll-vote-rating-approval.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-rating-approval/poll-vote-rating-approval.component.html b/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-rating-approval/poll-vote-rating-approval.component.html index ead712bfb0..cf9aa5fb65 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-rating-approval/poll-vote-rating-approval.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-rating-approval/poll-vote-rating-approval.component.html @@ -29,7 +29,7 @@ {{ 'Votes' | translate }}
} - @if (config()?.max_yes_amount < config()?.max_vote_sum) { + @if (config()?.max_yes_amount > 0 && config()?.max_yes_amount < config()?.max_vote_sum) {
{{ 'Available yes votes' | translate }}: {{ config()?.max_yes_amount - yesVotesUsed() }}/{{ config()?.max_yes_amount }} From 67e2697b832cff5b7558aa45add99ede3343736d Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Tue, 29 Sep 2026 11:09:28 +0200 Subject: [PATCH 15/26] Cleanup --- .../poll-vote-rating-approval.component.html | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-rating-approval/poll-vote-rating-approval.component.html b/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-rating-approval/poll-vote-rating-approval.component.html index cf9aa5fb65..47aa23c224 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-rating-approval/poll-vote-rating-approval.component.html +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-vote-rating-approval/poll-vote-rating-approval.component.html @@ -1,6 +1,6 @@
- +
@if (poll().option_ids.length > 1) {
@@ -15,10 +15,7 @@ } @else {
} - -
-
{{ 'Available votes' | translate }}: {{ availableVotes() }}/{{ config()?.max_vote_sum }} From 8997b75a6e94120fc92edd2bf406e97e20dc3173 Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Tue, 29 Sep 2026 18:47:34 +0200 Subject: [PATCH 16/26] Update yes_no 100% base --- .../poll-result-approval/poll-result-approval.component.ts | 6 +++++- .../pages/polls/view-models/poll-config-approval.ts | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) 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 5ab0bc35c2..36eaa43dd9 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 @@ -154,7 +154,11 @@ export class PollResultApprovalComponent extends PollResultBaseComponent(() => { - if (!this.config().onehundredPercentBaseNum) { + if ( + !this.config().onehundredPercentBaseNum || + this.onehundredPercentBase() === `yes_no` || + this.onehundredPercentBase() === `disabled` + ) { return null; } diff --git a/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-approval.ts b/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-approval.ts index 020dc7426d..116d9d9c4d 100644 --- a/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-approval.ts +++ b/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-approval.ts @@ -21,7 +21,11 @@ export class ViewPollConfigApproval extends BasePollConfigViewModel Date: Tue, 29 Sep 2026 19:03:55 +0200 Subject: [PATCH 17/26] Implement simple majority --- .../src/app/domain/models/poll/poll-config-rating-score.ts | 4 ++-- client/src/app/domain/models/poll/poll-config-selection.ts | 3 ++- client/src/app/domain/models/poll/poll-constants.ts | 3 ++- .../poll-result-selection.component.ts | 7 +++++++ 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/client/src/app/domain/models/poll/poll-config-rating-score.ts b/client/src/app/domain/models/poll/poll-config-rating-score.ts index 91769da7e1..076ff1f1cb 100644 --- a/client/src/app/domain/models/poll/poll-config-rating-score.ts +++ b/client/src/app/domain/models/poll/poll-config-rating-score.ts @@ -1,6 +1,6 @@ import { HasMeetingId } from '../../interfaces/has-meeting-id'; -import { PollRequiredMajority } from '.'; import { BasePollConfigModel } from './base-poll-config'; +import { SelectionRequiredMajority } from './poll-config-selection'; import { BaseOnehundredPercentBase } from './poll-config-types'; export type RatingScoreOnehundredPercentBase = BaseOnehundredPercentBase | `yes_no`; @@ -14,7 +14,7 @@ export class PollConfigRatingScore extends BasePollConfigModel) { super(PollConfigRatingScore.COLLECTION, input); diff --git a/client/src/app/domain/models/poll/poll-config-selection.ts b/client/src/app/domain/models/poll/poll-config-selection.ts index eadc0f062c..139f82e1e5 100644 --- a/client/src/app/domain/models/poll/poll-config-selection.ts +++ b/client/src/app/domain/models/poll/poll-config-selection.ts @@ -4,6 +4,7 @@ import { BasePollConfigModel } from './base-poll-config'; import { BaseOnehundredPercentBase } from './poll-config-types'; export type SelectionOnehundredPercentBase = BaseOnehundredPercentBase | `no_general`; +export type SelectionRequiredMajority = PollRequiredMajority | `simple_majority`; export class PollConfigSelection extends BasePollConfigModel { public static COLLECTION = `poll_config_selection`; @@ -14,7 +15,7 @@ export class PollConfigSelection extends BasePollConfigModel !isNaN(+k) && !isNaN(+result[k])) + .map(k => +result[k]) + ); } return null; From b972ca4a850c72b7ca4e5b758777e8ae5efe3022 Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Mon, 5 Oct 2026 18:53:02 +0200 Subject: [PATCH 18/26] Update meeting settings --- .../definitions/meeting-settings-defaults.ts | 8 - .../models/meetings/meeting-poll-default.ts | 37 -- .../models/meetings/meeting-poll-setting.ts | 43 +++ .../src/app/domain/models/meetings/meeting.ts | 18 - client/src/app/domain/translations/poll.ts | 2 +- ...meeting-poll-default-repository.service.ts | 19 -- ...meeting-poll-setting-repository.service.ts | 19 ++ .../definitions/relations/relations.ts | 8 +- .../site/pages/meetings/meetings.config.ts | 12 +- .../poll-config-form-base.component.ts | 3 + .../poll-form-rating-approval.component.html | 2 +- .../poll-form-rating-approval.component.ts | 36 +- .../poll-form-rating-score.component.html | 54 ++- .../poll-form-rating-score.component.ts | 5 + .../poll-form/poll-form.component.html | 22 +- .../poll-form/poll-form.component.ts | 19 +- .../poll-vote-rating-approval.component.ts | 16 +- .../poll-service-mapper.service/index.ts | 1 - .../poll-service-mapper.service.ts | 19 -- .../services/poll.service/poll.service.ts | 11 - .../topic-poll-dialog.component.html | 1 + .../topic-poll-dialog.component.ts | 12 +- .../topic-poll/services/topic-poll.service.ts | 36 +- .../assignment-poll-dialog.component.html | 1 + .../assignment-poll-dialog.component.ts | 12 +- .../services/assignment-poll.service.ts | 43 +-- .../motion-poll-dialog.component.html | 9 +- .../motion-poll-dialog.component.ts | 12 +- .../motion-poll.service.ts | 20 +- .../motion-add-poll-button.component.ts | 12 +- .../services/meeting-poll-settings.service.ts | 20 +- .../meeting-settings-definitions.ts | 316 ++++++++++-------- .../view-models/view-meeting-poll-default.ts | 17 - .../view-models/view-meeting-poll-setting.ts | 17 + .../meetings/view-models/view-meeting.ts | 117 ++++--- meta | 2 +- 36 files changed, 515 insertions(+), 486 deletions(-) delete mode 100644 client/src/app/domain/models/meetings/meeting-poll-default.ts create mode 100644 client/src/app/domain/models/meetings/meeting-poll-setting.ts delete mode 100644 client/src/app/gateways/repositories/meetings/meeting-poll-default-repository.service.ts create mode 100644 client/src/app/gateways/repositories/meetings/meeting-poll-setting-repository.service.ts delete mode 100644 client/src/app/site/pages/meetings/modules/poll/services/poll-service-mapper.service/index.ts delete mode 100644 client/src/app/site/pages/meetings/modules/poll/services/poll-service-mapper.service/poll-service-mapper.service.ts delete mode 100644 client/src/app/site/pages/meetings/view-models/view-meeting-poll-default.ts create mode 100644 client/src/app/site/pages/meetings/view-models/view-meeting-poll-setting.ts diff --git a/client/src/app/domain/definitions/meeting-settings-defaults.ts b/client/src/app/domain/definitions/meeting-settings-defaults.ts index 359dd6512b..5b2651fd5b 100644 --- a/client/src/app/domain/definitions/meeting-settings-defaults.ts +++ b/client/src/app/domain/definitions/meeting-settings-defaults.ts @@ -56,16 +56,8 @@ export const meetingSettingsDefaults: Record = { motions_enable_origin_motion_display: false, motions_origin_motion_toggle_default: false, assignment_poll_add_candidates_to_list_of_speakers: false, - poll_enable_max_yes_votes: false, - poll_enable_max_votes_per_option: false, - poll_default_live_voting_enabled: false, - poll_default_allow_invalid: false, - poll_default_allow_vote_split: false, - poll_default_required_majority: 'no_majority', poll_projection_name_order_first: 'last_name', poll_projection_max_columns: 6, - topic_poll_default_method: 'selection', - assignment_poll_default_method: 'selection', users_allow_self_set_present: true, users_enable_presence_view: false, users_enable_vote_weight: false, diff --git a/client/src/app/domain/models/meetings/meeting-poll-default.ts b/client/src/app/domain/models/meetings/meeting-poll-default.ts deleted file mode 100644 index e31b4bc2ae..0000000000 --- a/client/src/app/domain/models/meetings/meeting-poll-default.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { HasMeetingId } from '../../interfaces/has-meeting-id'; -import { BaseModel } from '../base/base-model'; -import { BaseOnehundredPercentBase } from '../poll/poll-config-types'; -import { PollVisibility } from '../poll/poll-constants'; - -export class MeetingPollDefault extends BaseModel { - public static COLLECTION = `meeting_poll_default`; - - public sort_result_by_votes: boolean; - - public allow_abstain!: boolean; - public allow_nota: boolean; - public visibility: PollVisibility; - public strike_out: boolean; - public onehundred_percent_base: BaseOnehundredPercentBase; - public group_ids: number[]; - public display_chart: string; - - public constructor(input?: Partial) { - super(MeetingPollDefault.COLLECTION, input); - } - - public static readonly REQUESTABLE_FIELDS: (keyof MeetingPollDefault)[] = [ - `id`, - `sort_result_by_votes`, - `visibility`, - `allow_abstain`, - `allow_nota`, - `strike_out`, - `onehundred_percent_base`, - `group_ids`, - `display_chart`, - `meeting_id` - ]; -} - -export interface MeetingPollDefault extends HasMeetingId {} diff --git a/client/src/app/domain/models/meetings/meeting-poll-setting.ts b/client/src/app/domain/models/meetings/meeting-poll-setting.ts new file mode 100644 index 0000000000..749e254914 --- /dev/null +++ b/client/src/app/domain/models/meetings/meeting-poll-setting.ts @@ -0,0 +1,43 @@ +import { HasMeetingId } from '../../interfaces/has-meeting-id'; +import { BaseModel } from '../base/base-model'; +import { BaseOnehundredPercentBase } from '../poll/poll-config-types'; +import { PollVisibility } from '../poll/poll-constants'; + +export class MeetingPollSetting extends BaseModel { + public static COLLECTION = `meeting_poll_setting`; + + public allow_live_voting: boolean; + public sort_result_by_votes: boolean; + + public method: string; // TODO: Enum + public required_majority: string; // TODO: Enum + public visibility: PollVisibility; + public onehundred_percent_base: BaseOnehundredPercentBase; + public group_ids: number[]; + public enable_max_yes_votes: boolean; + public enable_cumulative_voting: boolean; + public enable_max_options_limit: boolean; + public enable_live_voting: boolean; + + public constructor(input?: Partial) { + super(MeetingPollSetting.COLLECTION, input); + } + + public static readonly REQUESTABLE_FIELDS: (keyof MeetingPollSetting)[] = [ + `id`, + `sort_result_by_votes`, + `allow_live_voting`, + `enable_max_yes_votes`, + `enable_cumulative_voting`, + `enable_max_options_limit`, + `method`, + `visibility`, + `enable_live_voting`, + `required_majority`, + `onehundred_percent_base`, + `group_ids`, + `meeting_id` + ]; +} + +export interface MeetingPollSetting extends HasMeetingId {} diff --git a/client/src/app/domain/models/meetings/meeting.ts b/client/src/app/domain/models/meetings/meeting.ts index d1f349f09d..75e5e01f8f 100644 --- a/client/src/app/domain/models/meetings/meeting.ts +++ b/client/src/app/domain/models/meetings/meeting.ts @@ -165,22 +165,12 @@ export class Settings { public assignments_export_preamble!: string; public assignment_poll_add_candidates_to_list_of_speakers!: boolean; - public assignment_poll_default_method: string; public assignment_poll_config_id: Id; // topic poll - public topic_poll_default_method: string; 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` | `simple_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; @@ -414,17 +404,9 @@ export class Meeting extends BaseModel { `assignments_export_title`, `assignments_export_preamble`, `assignment_poll_add_candidates_to_list_of_speakers`, - `assignment_poll_default_method`, `assignment_poll_config_id`, `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`, diff --git a/client/src/app/domain/translations/poll.ts b/client/src/app/domain/translations/poll.ts index 982a397866..e450270852 100644 --- a/client/src/app/domain/translations/poll.ts +++ b/client/src/app/domain/translations/poll.ts @@ -23,7 +23,7 @@ export default { 'poll_percent_base.entitled': _(`All entitled users`), 'poll_percent_base.entitled_present': _(`Present entitled users`), 'poll_percent_base.disabled': _(`Disabled (no percents)`), - 'poll_required_majority.no_majority': _(`No majority`), + 'poll_required_majority.no_majority': _(`Deactivate`), 'poll_required_majority.two_third_majority': _(`Two thirds`), 'poll_required_majority.absolute_majority': _(`Absolute`), 'poll_required_majority.simple_majority': _(`Simple`) diff --git a/client/src/app/gateways/repositories/meetings/meeting-poll-default-repository.service.ts b/client/src/app/gateways/repositories/meetings/meeting-poll-default-repository.service.ts deleted file mode 100644 index 1473dc8795..0000000000 --- a/client/src/app/gateways/repositories/meetings/meeting-poll-default-repository.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from '@angular/core'; -import { MeetingPollDefault } from '@app/domain/models/meetings/meeting-poll-default'; -import { ViewMeetingPollDefault } from '@app/site/pages/meetings/view-models/view-meeting-poll-default'; - -import { BaseMeetingRelatedRepository } from '../base-meeting-related-repository'; - -@Injectable({ - providedIn: `root` -}) -export class MeetingPollDefaultRepositoryService extends BaseMeetingRelatedRepository< - ViewMeetingPollDefault, - MeetingPollDefault -> { - public baseModelCtor = MeetingPollDefault; - - public getTitle = (_viewMeetingPollDefault: ViewMeetingPollDefault): string => `Meeting poll default`; - - public getVerboseName = (): string => this.translate.instant(`Meeting poll default`); -} diff --git a/client/src/app/gateways/repositories/meetings/meeting-poll-setting-repository.service.ts b/client/src/app/gateways/repositories/meetings/meeting-poll-setting-repository.service.ts new file mode 100644 index 0000000000..59234c4e27 --- /dev/null +++ b/client/src/app/gateways/repositories/meetings/meeting-poll-setting-repository.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core'; +import { MeetingPollSetting } from '@app/domain/models/meetings/meeting-poll-setting'; +import { ViewMeetingPollSetting } from '@app/site/pages/meetings/view-models/view-meeting-poll-setting'; + +import { BaseMeetingRelatedRepository } from '../base-meeting-related-repository'; + +@Injectable({ + providedIn: `root` +}) +export class MeetingPollSettingRepositoryService extends BaseMeetingRelatedRepository< + ViewMeetingPollSetting, + MeetingPollSetting +> { + public baseModelCtor = MeetingPollSetting; + + public getTitle = (_viewMeetingPollDefault: ViewMeetingPollSetting): string => `Meeting poll setting`; + + public getVerboseName = (): string => this.translate.instant(`Meeting poll setting`); +} diff --git a/client/src/app/infrastructure/definitions/relations/relations.ts b/client/src/app/infrastructure/definitions/relations/relations.ts index 95318f293b..9722fe59f5 100644 --- a/client/src/app/infrastructure/definitions/relations/relations.ts +++ b/client/src/app/infrastructure/definitions/relations/relations.ts @@ -24,7 +24,7 @@ import { import { ViewPoll } from '@app/site/pages/meetings/pages/polls/view-models'; import { ViewPollBallotUser } from '@app/site/pages/meetings/pages/polls/view-models/poll-ballot-user'; import { ViewPollEntitledUser } from '@app/site/pages/meetings/pages/polls/view-models/poll-entitled-user'; -import { ViewMeetingPollDefault } from '@app/site/pages/meetings/view-models/view-meeting-poll-default'; +import { ViewMeetingPollSetting } from '@app/site/pages/meetings/view-models/view-meeting-poll-setting'; import { ViewMeetingUser } from '@app/site/pages/meetings/view-models/view-meeting-user'; import { ViewGender } from '@app/site/pages/organization/pages/accounts/pages/gender/view-models/view-gender'; import { ViewResource } from '@app/site/pages/organization/pages/resources/view-models'; @@ -196,19 +196,19 @@ export const RELATIONS: Relation[] = [ // ########## Meetings ...makeO2O({ AViewModel: ViewMeeting, - BViewModel: ViewMeetingPollDefault, + BViewModel: ViewMeetingPollSetting, AField: `assignment_poll_config`, BField: `meeting` }), ...makeO2O({ AViewModel: ViewMeeting, - BViewModel: ViewMeetingPollDefault, + BViewModel: ViewMeetingPollSetting, AField: `motion_poll_config`, BField: `meeting` }), ...makeO2O({ AViewModel: ViewMeeting, - BViewModel: ViewMeetingPollDefault, + BViewModel: ViewMeetingPollSetting, AField: `topic_poll_config`, BField: `meeting` }), diff --git a/client/src/app/site/pages/meetings/meetings.config.ts b/client/src/app/site/pages/meetings/meetings.config.ts index e604fcdae3..a941ae4e71 100644 --- a/client/src/app/site/pages/meetings/meetings.config.ts +++ b/client/src/app/site/pages/meetings/meetings.config.ts @@ -1,11 +1,11 @@ -import { MeetingPollDefault } from '@app/domain/models/meetings/meeting-poll-default'; +import { MeetingPollSetting } from '@app/domain/models/meetings/meeting-poll-setting'; import { MeetingRepositoryService } from '@app/gateways/repositories/meeting-repository.service'; -import { MeetingPollDefaultRepositoryService } from '@app/gateways/repositories/meetings/meeting-poll-default-repository.service'; +import { MeetingPollSettingRepositoryService } from '@app/gateways/repositories/meetings/meeting-poll-setting-repository.service'; import { Meeting } from '../../../domain/models/meetings/meeting'; import { AppConfig } from '../../../infrastructure/definitions/app-config'; import { ViewMeeting } from './view-models/view-meeting'; -import { ViewMeetingPollDefault } from './view-models/view-meeting-poll-default'; +import { ViewMeetingPollSetting } from './view-models/view-meeting-poll-setting'; export const MeetingsAppConfig: AppConfig = { name: `meeting`, @@ -16,9 +16,9 @@ export const MeetingsAppConfig: AppConfig = { repository: MeetingRepositoryService }, { - model: MeetingPollDefault, - viewModel: ViewMeetingPollDefault, - repository: MeetingPollDefaultRepositoryService + model: MeetingPollSetting, + viewModel: ViewMeetingPollSetting, + repository: MeetingPollSettingRepositoryService } ] }; 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 21bfbbb385..e95a3f55d6 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 @@ -4,11 +4,14 @@ import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { PollState } from '@app/domain/models/poll'; import { ViewPoll } from '@app/site/pages/meetings/pages/polls/view-models'; +import { ViewMeetingPollSetting } from '../../../view-models/view-meeting-poll-setting'; + @Component({ template: `` }) export abstract class PollFormBaseComponent { public form: UntypedFormGroup; public data = input.required>(); + public settings = input.required(); public formValid = signal(false); 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 4c93683056..4d5300784e 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 @@ -31,7 +31,7 @@
- @if (maxYesVotesEnabled()) { + @if (settings().enable_max_yes_votes) {
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 e772c32e65..6ae2a843fc 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,4 @@ -import { Component, effect, inject, input } from '@angular/core'; +import { 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'; @@ -8,7 +8,6 @@ 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 { 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'; @@ -50,9 +49,14 @@ export class PollFormRatingApprovalComponent extends PollFormBaseComponent { public hideMethod = input(false); public optionAmount = input(null); - private meetingSettingsService = inject(MeetingSettingsService); + public getSerialzedForm(): Record { + const formValue = this.form.value; + if (!this.settings().enable_max_yes_votes) { + delete formValue.max_yes_amount; + } - public maxYesVotesEnabled = this.meetingSettingsService.signal(`poll_enable_max_yes_votes`); + return formValue; + } protected initForm(): void { const fields = { @@ -60,14 +64,13 @@ export class PollFormRatingApprovalComponent extends PollFormBaseComponent { allow_abstain: [false], max_options_amount: [1, [Validators.required, Validators.min(1)]], min_options_amount: [1, [Validators.required, Validators.min(0), this.minOptionsAmountValidator()]], - required_majority: [`no_majority`] + required_majority: [`no_majority`], + max_yes_amount: [1] }; - if (inject(MeetingSettingsService).instant(`poll_enable_max_yes_votes`)) { - fields[`max_yes_amount`] = [1, [Validators.required, Validators.min(1)]]; - } this.form = this.fb.group(fields); effect(this.onOptionAmountUpdate.bind(this)); + effect(this.onSettingsUpdated.bind(this)); this.form .get(`max_options_amount`) @@ -94,15 +97,6 @@ export class PollFormRatingApprovalComponent extends PollFormBaseComponent { return patch; } - public getSerialzedForm(): Record { - const formValue = this.form.value; - if (!this.maxYesVotesEnabled()) { - delete formValue.max_yes_amount; - } - - return formValue; - } - private minOptionsAmountValidator(): ValidatorFn { return (field: AbstractControl): ValidationErrors | null => { const min = Number(field.getRawValue()); @@ -137,4 +131,12 @@ export class PollFormRatingApprovalComponent extends PollFormBaseComponent { maxCtrl?.updateValueAndValidity({ emitEvent: false }); maxYesCtrl?.updateValueAndValidity({ emitEvent: false }); } + + private onSettingsUpdated(): void { + if (this.settings().enable_max_yes_votes) { + this.form.get(`max_yes_amount`).setValidators([Validators.required, Validators.min(1)]); + } else { + this.form.get(`max_yes_amount`).setValidators([]); + } + } } 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 03dcaf22d6..b9c201f45a 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 @@ -2,17 +2,13 @@
@if (!pollStarted()) {
- @if (!form.get('allow_general_abstain').value) { - - {{ 'Min amount of votes' | translate }} - - @if (form.get('min_options_amount').hasError('minGreaterThanMax')) { - {{ 'Amount must not exceed max amount of votes' | translate }} - } - - } @else { -
- } + + {{ 'Min amount of votes' | translate }} + + @if (form.get('min_options_amount').hasError('minGreaterThanMax')) { + {{ 'Amount must not exceed max amount of votes' | translate }} + } + {{ 'Max amount of votes' | translate }} @@ -20,30 +16,30 @@
-
- @if (!form.get('allow_general_abstain').value) { + @if (settings().enable_max_options_limit) { +
{{ 'Min amount of options voted' | translate }} - + @if (form.get('min_options_amount').hasError('minGreaterThanMax')) { {{ 'Amount must not exceed max amount of options' | translate }} } - } @else { -
- } - - {{ '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 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 }} + + } + +
+ }
@@ -54,7 +50,7 @@
- + {{ 'General abstain' | 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 dd37ea68bc..583ef36436 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 @@ -53,6 +53,11 @@ export class PollFormRatingScoreComponent extends PollFormBaseComponent { }; delete data[`allow_empty`]; + if (!this.settings().enable_max_options_limit) { + delete data[`min_options_amount`]; + delete data[`max_options_amount`]; + } + return data; } 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 96fe01ae2c..fb6ad7cc06 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 @@ -120,16 +120,30 @@

@switch (this.selectedMethod()) { @case ('approval') { - + } @case ('selection') { - + } @case ('rating_approval') { - + } @case ('rating_score') { - + } }
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 d72287e07b..0705d34da0 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 @@ -13,7 +13,7 @@ import { PollVisibility } from '@app/domain/models/poll'; import { infoDialogSettings } from '@app/infrastructure/utils/dialog-settings'; import { BaseComponent } from '@app/site/base/base.component'; import { ViewPoll } from '@app/site/pages/meetings/pages/polls/view-models'; -import { MeetingSettingsService } from '@app/site/pages/meetings/services/meeting-settings.service'; +import { ViewMeetingPollSetting } from '@app/site/pages/meetings/view-models/view-meeting-poll-setting'; import { DirectivesModule } from '@app/ui/directives'; import { EditableListComponent } from '@app/ui/modules/editable-list'; import { SearchSelectorModule } from '@app/ui/modules/search-selector'; @@ -81,9 +81,11 @@ export class PollFormComponent extends BaseComponent { public optionEdit = input(false); public isEVotingEnabled = input.required(); + public pollSettings = input.required(); + public sortFn = (groupA: ViewGroup, groupB: ViewGroup): number => groupA.weight - groupB.weight; - public readonly data = input>({}); + public readonly data = input & { method_preselection?: string }>({}); private pollModel = signal({ title: ``, @@ -149,14 +151,19 @@ export class PollFormComponent extends BaseComponent { }); public isLiveVotingAvailable = computed(() => { - return this.isEVotingSelected() && (this.isNamedVotingSelected() || this.isOpenVotingSelected()); + return ( + this.isEVotingSelected() && + (this.isNamedVotingSelected() || this.isOpenVotingSelected()) && + this.pollSettings().allow_live_voting + ); }); public groupRepo = inject(GroupControllerService); private dialog = inject(MatDialog); - private meetingSettingsService = inject(MeetingSettingsService); - public allowCumulative = this.meetingSettingsService.signal(`poll_enable_max_votes_per_option`); + public allowCumulative = computed(() => { + return this.pollSettings().enable_cumulative_voting; + }); public constructor() { super(); @@ -212,6 +219,8 @@ export class PollFormComponent extends BaseComponent { private updateData(): void { const data = this.data(); if (data && this.form) { + if (data.method_preselection !== undefined && !this.form['method_preselection']().dirty()) + this.form['method_preselection']().value.set(data.method_preselection); if (data.entitled_group_ids !== undefined && !this.form['entitled_group_ids']().dirty()) this.form['entitled_group_ids']().value.set(data.entitled_group_ids); if (data.live_voting_enabled !== undefined && !this.form['live_voting_enabled']().dirty()) 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 01712727c1..73b7aa1a84 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 @@ -1,9 +1,7 @@ -import { NgTemplateOutlet } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core'; 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'; @@ -14,14 +12,7 @@ import { PollVoteOptionComponent } from '../poll-vote-option/poll-vote-option.co @Component({ selector: 'os-poll-vote-rating-approval', - imports: [ - PollVoteButtonComponent, - PollVoteOptionComponent, - NgTemplateOutlet, - MatIconModule, - MatButtonModule, - TranslatePipe - ], + imports: [PollVoteButtonComponent, PollVoteOptionComponent, MatIconModule, MatButtonModule, TranslatePipe], templateUrl: './poll-vote-rating-approval.component.html', styleUrl: './poll-vote-rating-approval.component.scss', changeDetection: ChangeDetectionStrategy.OnPush @@ -29,7 +20,6 @@ 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()); @@ -42,10 +32,6 @@ 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/services/poll-service-mapper.service/index.ts b/client/src/app/site/pages/meetings/modules/poll/services/poll-service-mapper.service/index.ts deleted file mode 100644 index fbd4210ffe..0000000000 --- a/client/src/app/site/pages/meetings/modules/poll/services/poll-service-mapper.service/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './poll-service-mapper.service'; diff --git a/client/src/app/site/pages/meetings/modules/poll/services/poll-service-mapper.service/poll-service-mapper.service.ts b/client/src/app/site/pages/meetings/modules/poll/services/poll-service-mapper.service/poll-service-mapper.service.ts deleted file mode 100644 index abdb18764b..0000000000 --- a/client/src/app/site/pages/meetings/modules/poll/services/poll-service-mapper.service/poll-service-mapper.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Service } from '@angular/core'; - -import { PollService } from '../poll.service/poll.service'; - -@Service() -export class PollServiceMapperService { - private _registry: Record = {}; - - public registerService(collection: string, service: PollService): void { - this._registry[collection] = service; - } - - public getService(collection?: string): PollService | null { - if (!collection || !this._registry[collection]) { - return null; - } - return this._registry[collection]; - } -} diff --git a/client/src/app/site/pages/meetings/modules/poll/services/poll.service/poll.service.ts b/client/src/app/site/pages/meetings/modules/poll/services/poll.service/poll.service.ts index 5cdd7c9596..a87b5117d8 100644 --- a/client/src/app/site/pages/meetings/modules/poll/services/poll.service/poll.service.ts +++ b/client/src/app/site/pages/meetings/modules/poll/services/poll.service/poll.service.ts @@ -24,15 +24,8 @@ const PERCENT_DECIMAL_PLACES = 3; @Service() export abstract class PollService { - protected sortByVote = false; protected enableMaxVotesPerOption = false; - public get defaultPollLiveVotingEnabled(): boolean { - return this._defaultPollLiveVotingEnabled; - } - - private _defaultPollLiveVotingEnabled = false; - public get isElectronicVotingEnabled(): boolean { return this._isElectronicVotingEnabled; } @@ -49,10 +42,6 @@ export abstract class PollService { this.organizationSettingsService .get(`enable_electronic_voting`) .subscribe(is => (this._isElectronicVotingEnabled = is)); - - this.meetingSettingsService - .get(`poll_default_live_voting_enabled`) - .subscribe(is => (this._defaultPollLiveVotingEnabled = is)); } public generateTableData(_poll: ViewPoll): PollTableData[] { 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 9baf638a93..36890386d5 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 @@ -5,6 +5,7 @@ [isEVotingEnabled]="isEVotingEnabled" [optionAmount]="options().length" [optionEdit]="true" + [pollSettings]="pollSettings | async" >

diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-dialog/topic-poll-dialog.component.ts b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-dialog/topic-poll-dialog.component.ts index 2bc52a40a2..934716e34b 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-dialog/topic-poll-dialog.component.ts +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/modules/topic-poll/components/topic-poll-dialog/topic-poll-dialog.component.ts @@ -1,3 +1,4 @@ +import { AsyncPipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatDialogModule } from '@angular/material/dialog'; @@ -11,7 +12,10 @@ import { import { PollEditResultComponent } from '@app/site/pages/meetings/modules/poll/components/poll-edit-result/poll-edit-result.component'; import { PollFormComponent } from '@app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component'; import { PollService } from '@app/site/pages/meetings/modules/poll/services/poll.service'; +import { ActiveMeetingService } from '@app/site/pages/meetings/services/active-meeting.service'; +import { ViewMeetingPollSetting } from '@app/site/pages/meetings/view-models/view-meeting-poll-setting'; import { TranslatePipe } from '@ngx-translate/core'; +import { Observable, switchMap } from 'rxjs'; @Component({ selector: `os-topic-poll-dialog`, @@ -23,7 +27,8 @@ import { TranslatePipe } from '@ngx-translate/core'; MatTabsModule, MatDialogModule, MatButtonModule, - TranslatePipe + TranslatePipe, + AsyncPipe ], changeDetection: ChangeDetectionStrategy.Eager }) @@ -38,7 +43,12 @@ export class TopicPollDialogComponent extends BasePollDialogComponent { return this.pollForm().form.options().value(); }); + public get pollSettings(): Observable { + return this.activeMeetingService.meetingObservable.pipe(switchMap(m => m.topic_poll_config$)); + } + private pollService = inject(PollService); + private activeMeetingService = inject(ActiveMeetingService); public override methodPayload(): PollMethodPayload { return { 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 e61161f0fa..9e2b8bedaa 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 @@ -3,12 +3,9 @@ import { PollVisibility } from '@app/domain/models/poll'; 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'; -import { PollServiceMapperService } from '@app/site/pages/meetings/modules/poll/services/poll-service-mapper.service'; import { ViewPoll } from '@app/site/pages/meetings/pages/polls/view-models'; import { MeetingPollSettingsService } from '@app/site/pages/meetings/services/meeting-poll-settings.service'; -import { ViewTopic } from '../../../view-models/view-topic'; - @Service() export class TopicPollService extends PollService { private pollRepo = inject(PollControllerService); @@ -16,39 +13,20 @@ export class TopicPollService extends PollService { 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 defaultEnableLiveVote = this.meetingPollSettingsService.signal(`topic`, `enable_live_voting`); 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`, `sort_result_by_votes`) - .subscribe(sort => (this.sortByVote = sort)); - } + private defaultRequiredMajority = this.meetingPollSettingsService.signal(`topic`, `required_majority`); + private defaultMethod = this.meetingPollSettingsService.signal(`topic`, `method`); - public getDefaultPollData(contentObject?: Topic): Partial { - const poll: Partial = { + public getDefaultPollData(contentObject?: Topic): Partial & { method_preselection?: string } { + const poll: Partial & { method_preselection?: string } = { title: this.translate.instant(`Vote`), - entitled_group_ids: Object.values(this.defaultGroupIds() ?? []), visibility: this.isElectronicVotingEnabled ? this.defaultPollType() : PollVisibility.Manually, + entitled_group_ids: Object.values(this.defaultGroupIds() ?? []), live_voting_enabled: this.defaultEnableLiveVote(), + method_preselection: this.defaultMethod(), 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(), 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.html b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html index d8f11655a8..1957f376b6 100644 --- a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html +++ b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html @@ -5,6 +5,7 @@ [data]="pollData" [isEVotingEnabled]="isEVotingEnabled" [optionAmount]="optionAmount" + [pollSettings]="pollSettings | async" >
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 d0ddd97915..f365adca97 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 @@ -1,3 +1,4 @@ +import { AsyncPipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatDialogModule } from '@angular/material/dialog'; @@ -11,7 +12,10 @@ import { PollEditResultComponent } from '@app/site/pages/meetings/modules/poll/c import { PollFormComponent } from '@app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component'; import { PollService } from '@app/site/pages/meetings/modules/poll/services/poll.service'; import { ViewAssignment } from '@app/site/pages/meetings/pages/assignments'; +import { ActiveMeetingService } from '@app/site/pages/meetings/services/active-meeting.service'; +import { ViewMeetingPollSetting } from '@app/site/pages/meetings/view-models/view-meeting-poll-setting'; import { TranslatePipe } from '@ngx-translate/core'; +import { Observable, switchMap } from 'rxjs'; @Component({ selector: `os-assignment-poll-dialog`, @@ -23,7 +27,8 @@ import { TranslatePipe } from '@ngx-translate/core'; MatDialogModule, MatButtonModule, MatTabsModule, - TranslatePipe + TranslatePipe, + AsyncPipe ], changeDetection: ChangeDetectionStrategy.Eager }) @@ -42,7 +47,12 @@ export class AssignmentPollDialogComponent extends BasePollDialogComponent { return assignment.candidates.length; } + public get pollSettings(): Observable { + return this.activeMeetingService.meetingObservable.pipe(switchMap(m => m.assignment_poll_config$)); + } + private pollService = inject(PollService); + private activeMeetingService = inject(ActiveMeetingService); public override methodPayload(): PollMethodPayload { return { 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 0e9aedbb0f..7d66175081 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,8 +1,6 @@ import { inject, Service } from '@angular/core'; import { Assignment } from '@app/domain/models/assignments/assignment'; import { PollVisibility } from '@app/domain/models/poll'; -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'; import { MeetingPollSettingsService } from '@app/site/pages/meetings/services/meeting-poll-settings.service'; import { _ } from '@ngx-translate/core'; @@ -22,43 +20,22 @@ export class AssignmentPollService extends PollService { 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`); + private defaultPercentBase = this.meetingPollSettingsService.signal(`assignment`, `onehundred_percent_base`); + private defaultGroupIds = this.meetingPollSettingsService.signal(`assignment`, `group_ids`); + private defaultEnableLiveVote = this.meetingPollSettingsService.signal(`assignment`, `enable_live_voting`); + private defaultPollType = this.meetingPollSettingsService.signal(`assignment`, `visibility`); + private defaultRequiredMajority = this.meetingPollSettingsService.signal(`assignment`, `required_majority`); + private defaultMethod = this.meetingPollSettingsService.signal(`assignment`, `method`); - public constructor() { - super(); - const pollServiceMapper = inject(PollServiceMapperService); - pollServiceMapper.registerService(ViewAssignment.COLLECTION, this); - this.meetingPollSettingsService - .get(`assignment`, `sort_result_by_votes`) - .subscribe(sort => (this.sortByVote = sort)); - - this.meetingSettingsService - .get(`poll_enable_max_votes_per_option`) - .subscribe(enable_max_votes_per_option => (this.enableMaxVotesPerOption = enable_max_votes_per_option)); - } - - public getDefaultPollData(contentObject?: Assignment): Partial { - const poll: Partial = { + public getDefaultPollData(contentObject?: Assignment): Partial & { method_preselection?: string } { + const poll: Partial & { method_preselection?: string } = { title: this.translate.instant(`Ballot`), - entitled_group_ids: Object.values(this.defaultGroupIds() ?? []), visibility: this.isElectronicVotingEnabled ? this.defaultPollType() : PollVisibility.Manually, + entitled_group_ids: Object.values(this.defaultGroupIds() ?? []), live_voting_enabled: this.defaultEnableLiveVote(), + method_preselection: this.defaultMethod(), 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(), required_majority: this.defaultRequiredMajority() } }; diff --git a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.html b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.html index 20ec623407..8c24e0427c 100644 --- a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.html +++ b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.html @@ -1,7 +1,12 @@
- - + +
diff --git a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.ts b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.ts index f8365a0f4a..2da32d5fe7 100644 --- a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.ts +++ b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.ts @@ -1,3 +1,4 @@ +import { AsyncPipe } from '@angular/common'; import { Component, computed, inject, viewChild } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatDialogModule } from '@angular/material/dialog'; @@ -11,7 +12,10 @@ import { PollEditResultComponent } from '@app/site/pages/meetings/modules/poll/c import { PollFormComponent } from '@app/site/pages/meetings/modules/poll/components/poll-form/poll-form.component'; import { PollFormApprovalComponent } from '@app/site/pages/meetings/modules/poll/components/poll-form-approval/poll-form-approval.component'; import { PollService } from '@app/site/pages/meetings/modules/poll/services/poll.service'; +import { ActiveMeetingService } from '@app/site/pages/meetings/services/active-meeting.service'; +import { ViewMeetingPollSetting } from '@app/site/pages/meetings/view-models/view-meeting-poll-setting'; import { TranslatePipe } from '@ngx-translate/core'; +import { Observable, switchMap } from 'rxjs'; @Component({ selector: `os-motion-poll-dialog`, @@ -22,7 +26,8 @@ import { TranslatePipe } from '@ngx-translate/core'; PollFormApprovalComponent, MatDialogModule, MatButtonModule, - TranslatePipe + TranslatePipe, + AsyncPipe ], styleUrls: [`./motion-poll-dialog.component.scss`] }) @@ -41,7 +46,12 @@ export class MotionPollDialogComponent extends BasePollDialogComponent { return this.approvalForm().form.value; } + public get pollSettings(): Observable { + return this.activeMeetingService.meetingObservable.pipe(switchMap(m => m.motion_poll_config$)); + } + private pollService = inject(PollService); + private activeMeetingService = inject(ActiveMeetingService); public override methodPayload(): PollMethodPayload { const config = { ...this.approvalFormValue }; 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 94552638f3..832bb08d84 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 @@ -15,22 +15,20 @@ export class MotionPollService extends PollService { 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(); - } + private defaultMethod = this.meetingPollSettingsService.signal(`motion`, `method`); + private defaultLiveVoting = this.meetingPollSettingsService.signal(`motion`, `enable_live_voting`); + private defaultRequiredMajority = this.meetingPollSettingsService.signal(`motion`, `required_majority`); + private defaultPercentBase = this.meetingPollSettingsService.signal(`motion`, `onehundred_percent_base`); - public getDefaultPollData(contentObject?: Motion): Partial { - const poll: Partial = { - entitled_group_ids: Object.values(this.defaultGroupIds() ?? []), + public getDefaultPollData(contentObject?: Motion): Partial & { method_preselection?: string } { + const poll: Partial & { method_preselection?: string } = { visibility: this.isElectronicVotingEnabled ? this.defaultPollVisibility() : PollVisibility.Manually, + entitled_group_ids: Object.values(this.defaultGroupIds() ?? []), + live_voting_enabled: this.defaultLiveVoting(), config: { - allow_abstain: this.defaultAllowAbstain(), + allow_abstain: this.defaultMethod().endsWith(`yes_no_abstain`), onehundred_percent_base: this.defaultPercentBase(), required_majority: this.defaultRequiredMajority() } diff --git a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-view/components/motion-add-poll-button/motion-add-poll-button.component.ts b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-view/components/motion-add-poll-button/motion-add-poll-button.component.ts index 4253c394dd..56e2784e16 100644 --- a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-view/components/motion-add-poll-button/motion-add-poll-button.component.ts +++ b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-view/components/motion-add-poll-button/motion-add-poll-button.component.ts @@ -1,7 +1,8 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, Input } from '@angular/core'; import { Permission } from '@app/domain/definitions/permission'; import { PollDialogData } from '@app/site/pages/meetings/modules/poll/definitions'; import { ViewMotion } from '@app/site/pages/meetings/pages/motions'; +import { MeetingPollSettingsService } from '@app/site/pages/meetings/services/meeting-poll-settings.service'; import { map, Observable } from 'rxjs'; import { MotionPollService } from '../../../../../../modules/motion-poll/services/motion-poll.service/motion-poll.service'; @@ -24,10 +25,9 @@ export class MotionAddPollButtonComponent { return this.motion.state$.pipe(map(state => state?.allow_create_poll || false)); } - public constructor( - private motionPollService: MotionPollService, - private pollDialog: MotionPollDialogService - ) {} + private motionPollService = inject(MotionPollService); + private pollDialog = inject(MotionPollDialogService); + private meetingPollSettingsService = inject(MeetingPollSettingsService); public async openDialog(): Promise { const defaultPollData = this.motionPollService.getDefaultPollData(this.motion); @@ -35,7 +35,7 @@ export class MotionAddPollButtonComponent { content_object_id: this.motion.fqid, content_object: this.motion, ...defaultPollData, - live_voting_enabled: this.motionPollService.defaultPollLiveVotingEnabled + live_voting_enabled: this.meetingPollSettingsService.instant(`motion`, `enable_live_voting`) }; this.pollDialog.open(dialogData); 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 f2b3cc228f..192d834f5e 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,5 +1,5 @@ import { inject, Service, Signal, signal, WritableSignal } from '@angular/core'; -import { MeetingPollDefault } from '@app/domain/models/meetings/meeting-poll-default'; +import { MeetingPollSetting } from '@app/domain/models/meetings/meeting-poll-setting'; import { BehaviorSubject, combineLatest, filter, Observable, switchMap } from 'rxjs'; import { ActiveMeetingService } from './active-meeting.service'; @@ -92,10 +92,10 @@ export class MeetingPollSettingsService { * * @param key The setting value to get from. */ - public instant( + public instant( collection: MeetingPollSettingCollection, key: T - ): MeetingPollDefault[T] | null { + ): MeetingPollSetting[T] | null { return this.activeMeetingService.meeting[`${collection}_poll_config`][key]; } @@ -104,14 +104,14 @@ export class MeetingPollSettingsService { * * @param key The setting value to get from. */ - public get( + public get( collection: MeetingPollSettingCollection, key: T - ): Observable { + ): Observable { if (!this.settingSubjects[collection][key]) { this.settingSubjects[collection][key] = new BehaviorSubject(this.instant(collection, key)); } - return this.settingSubjects[collection][key] as Observable; + return this.settingSubjects[collection][key] as Observable; } /** @@ -119,13 +119,13 @@ export class MeetingPollSettingsService { * * @param key The setting value to get from. */ - public signal( + public signal( collection: MeetingPollSettingCollection, key: T - ): Signal { + ): Signal { if (!this.settingSignals[collection][key]) { - this.settingSignals[collection][key] = signal(this.instant(collection, key)); + this.settingSignals[collection][key] = signal(this.instant(collection, key)); } - return this.settingSignals[collection][key] as Signal; + return this.settingSignals[collection][key] as Signal; } } diff --git a/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definitions.ts b/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definitions.ts index 6391f75679..efe85650c8 100644 --- a/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definitions.ts +++ b/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definitions.ts @@ -825,39 +825,6 @@ export const meetingSettings: SettingsGroup[] = fillInSettingsDefaults([ { label: _(`General`), settings: [ - { - key: `poll_enable_max_yes_votes`, - label: _(`Allow limiting the amount of yes votes in per candidate polls`), - type: `boolean` - }, - { - key: `poll_enable_max_votes_per_option`, - label: _(`Allow to accumulate several votes on one candidate or option ("comulative voting")`), - type: `boolean` - }, - { - key: `poll_default_live_voting_enabled`, - label: _(`Set live voting enabled by default`), - type: `boolean`, - helpText: _(`Only available for nominal and open voting`) - }, - { - key: `poll_default_allow_invalid`, - label: _(`Set invalid votes allowed by default`), - type: `boolean` - }, - { - key: `poll_default_allow_vote_split`, - label: _(`Set vote split allowed by default`), - type: `boolean` - }, - { - key: `poll_default_required_majority`, - label: _(`Default required majority`), - type: `choice`, - choiceI18nPrefix: 'poll_required_majority', - choices: [`no_majority`, `two_third_majority`, `absolute_majority`] - }, { key: `poll_projection_name_order_first`, label: _(`Sort participant names on single votes projection by`), @@ -879,16 +846,25 @@ export const meetingSettings: SettingsGroup[] = fillInSettingsDefaults([ label: _(`Agenda`), settings: [ { - key: `topic_poll_default_method`, - label: _(`Default poll type`), + key: `topic_poll_setting_visibility`, + subscriptionKey: { + idField: `topic_poll_config_id`, + field: `visibility` + }, + label: _(`Default voting type`), type: `choice`, - choices: { - selection: _(`Selection`), - approval: _(`Approval`) + choiceI18nPrefix: 'poll_visibility', + choices: ['manually', 'named', 'open', 'secret'], + restrictionFn: (orgaSettings, value: any): any => { + const isElectronicVotingEnabled = orgaSettings.instant(`enable_electronic_voting`); + if (!isElectronicVotingEnabled && typeof value !== `string`) { + return [`manually`]; + } + return value; } }, { - key: `topic_poll_default_group_ids`, + key: `topic_poll_setting_group_ids`, subscriptionKey: { idField: `topic_poll_config_id`, field: `group_ids` @@ -897,87 +873,97 @@ export const meetingSettings: SettingsGroup[] = fillInSettingsDefaults([ type: `groups` }, { - key: `topic_poll_default_visibility`, + key: `topic_poll_setting_method`, subscriptionKey: { idField: `topic_poll_config_id`, - field: `visibility` + field: `method` }, - label: _(`Default voting type`), + label: _(`Default agenda voting method`), type: `choice`, - choiceI18nPrefix: 'poll_visibility', - choices: ['manually', 'named', 'open', 'secret'], - restrictionFn: (orgaSettings, value: any): any => { - const isElectronicVotingEnabled = orgaSettings.instant(`enable_electronic_voting`); - if (!isElectronicVotingEnabled && typeof value !== `string`) { - return [`manually`]; - } - return value; + choices: { + 'approval.yes_no': _(`Yes/No`), + 'approval.yes_no_abstain': _(`Yes/No/Abstain`), + 'selection.yes': _(`Yes per option`), + rating_score: _(`Amount per option`) } }, { - key: `topic_poll_default_allow_abstain`, + key: `topic_poll_setting_enable_max_yes_votes`, subscriptionKey: { idField: `topic_poll_config_id`, - field: `allow_abstain` + field: `enable_max_yes_votes` }, - label: _(`Use poll method Yes/No/Abstain by default`), + label: _(`Allow limiting the amount of yes votes in per candidate polls`), type: `boolean` - // TODO: Disable depending on vote method }, { - key: `topic_poll_default_allow_nota`, + key: `topic_poll_setting_enable_cumulative_voting`, subscriptionKey: { idField: `topic_poll_config_id`, - field: `allow_nota` + field: `enable_cumulative_voting` }, - label: _(`Allow general option by default`), + label: _(`Allow to accumulate several votes on one option ("cumulative voting")`), type: `boolean` - // TODO: Disable depending on vote method }, { - key: `topic_poll_default_strike_out`, + key: `topic_poll_setting_enable_max_options_limit`, + subscriptionKey: { + idField: `topic_poll_config_id`, + field: `enable_max_options_limit` + }, + label: _(`Allow to limit options that can be voted`), + type: `boolean`, + indentation: 1 + }, + { + key: `topic_poll_setting_allow_live_voting`, subscriptionKey: { idField: `topic_poll_config_id`, - field: `strike_out` + field: `allow_live_voting` }, - label: _(`Use disapproval voting by default`), + label: _(`Activate live voting`), type: `boolean` - // TODO: Disable depending on vote method }, { - key: `topic_poll_default_onehundred_percent_base`, + key: `topic_poll_setting_enable_live_voting`, subscriptionKey: { idField: `topic_poll_config_id`, - field: `onehundred_percent_base` + field: `enable_live_voting` }, - label: _(`Default 100 % base`), + label: _(`Set live voting as default`), + type: `boolean`, + indentation: 1 + }, + { + key: `topic_poll_setting_required_majority`, + subscriptionKey: { + idField: `topic_poll_config_id`, + field: `required_majority` + }, + label: _(`Default required majority`), type: `choice`, - choiceI18nPrefix: 'poll_percent_base', - choices: ['yes_no', 'valid', 'cast' /* , 'entitled', 'entitled_present' */, 'disabled'] + choiceI18nPrefix: 'poll_required_majority', + choices: ['no_majority', 'simple_majority', 'absolute_majority', 'two_third_majority'] }, { - key: `topic_poll_default_display_chart`, + key: `topic_poll_setting_onehundred_percent_base`, subscriptionKey: { idField: `topic_poll_config_id`, - field: `display_chart` + field: `onehundred_percent_base` }, - label: _(`Default poll chart type`), + label: _(`Default 100 % base`), type: `choice`, - choices: { - table: _(`Table`), - pie: _(`Pie chart`) - } - // TODO: Disable depending on vote method + choiceI18nPrefix: 'poll_percent_base', + choices: ['yes_no', 'valid', 'cast', 'no_general', 'entitled', 'entitled_present', 'disabled'] }, { - key: `topic_poll_default_sort_result_by_votes`, + key: `topic_poll_setting_sort_result_by_votes`, subscriptionKey: { idField: `topic_poll_config_id`, field: `sort_result_by_votes` }, label: _(`Sort election results by amount of votes`), type: `boolean` - // TODO: Disable depending on vote method } ] }, @@ -985,16 +971,7 @@ export const meetingSettings: SettingsGroup[] = fillInSettingsDefaults([ label: _(`Motion`), settings: [ { - key: `motion_poll_default_group_ids`, - subscriptionKey: { - idField: `motion_poll_config_id`, - field: `group_ids` - }, - label: _(`Default groups with voting rights`), - type: `groups` - }, - { - key: `motion_poll_default_visibility`, + key: `motion_poll_setting_visibility`, subscriptionKey: { idField: `motion_poll_config_id`, field: `visibility` @@ -1012,16 +989,59 @@ export const meetingSettings: SettingsGroup[] = fillInSettingsDefaults([ } }, { - key: `motion_poll_default_allow_abstain`, + key: `motion_poll_setting_group_ids`, subscriptionKey: { idField: `motion_poll_config_id`, - field: `allow_abstain` + field: `group_ids` }, - label: _(`Use poll method Yes/No/Abstain by default`), + label: _(`Default groups with voting rights`), + type: `groups` + }, + { + key: `motion_poll_setting_method`, + subscriptionKey: { + idField: `motion_poll_config_id`, + field: `method` + }, + label: _(`Default motion voting method`), + type: `choice`, + choices: { + 'approval.yes_no': _(`Yes/No`), + 'approval.yes_no_abstain': _(`Yes/No/Abstain`) + } + }, + { + key: `motion_poll_setting_allow_live_voting`, + subscriptionKey: { + idField: `motion_poll_config_id`, + field: `allow_live_voting` + }, + label: _(`Activate live voting`), type: `boolean` }, { - key: `motion_poll_default_onehundred_percent_base`, + key: `motion_poll_setting_enable_live_voting`, + subscriptionKey: { + idField: `motion_poll_config_id`, + field: `enable_live_voting` + }, + label: _(`Set live voting as default`), + type: `boolean`, + indentation: 1 + }, + { + key: `motion_poll_setting_required_majority`, + subscriptionKey: { + idField: `motion_poll_config_id`, + field: `required_majority` + }, + label: _(`Default required majority`), + type: `choice`, + choiceI18nPrefix: 'poll_required_majority', + choices: ['no_majority', 'simple_majority', 'absolute_majority', 'two_third_majority'] + }, + { + key: `motion_poll_setting_onehundred_percent_base`, subscriptionKey: { idField: `motion_poll_config_id`, field: `onehundred_percent_base` @@ -1029,7 +1049,7 @@ export const meetingSettings: SettingsGroup[] = fillInSettingsDefaults([ label: _(`Default 100 % base`), type: `choice`, choiceI18nPrefix: 'poll_percent_base', - choices: ['yes_no', 'valid', 'cast' /* , 'entitled', 'entitled_present' */, 'disabled'] + choices: ['yes_no', 'valid', 'cast', 'entitled', 'entitled_present', 'disabled'] } ] }, @@ -1037,18 +1057,25 @@ export const meetingSettings: SettingsGroup[] = fillInSettingsDefaults([ label: _(`Elections`), settings: [ { - key: `assignment_poll_default_method`, - label: _(`Default poll type`), + key: `assignment_poll_setting_visibility`, + subscriptionKey: { + idField: `assignment_poll_config_id`, + field: `visibility` + }, + label: _(`Default voting type`), type: `choice`, - choices: { - selection: _(`Selection`), - rating_approval: _(`Approval per candidate`), - rating_score: _(`Cumulative`), - approval: _(`List`) + choiceI18nPrefix: 'poll_visibility', + choices: ['manually', 'named', 'open', 'secret'], + restrictionFn: (orgaSettings, value: any): any => { + const isElectronicVotingEnabled = orgaSettings.instant(`enable_electronic_voting`); + if (!isElectronicVotingEnabled && typeof value !== `string`) { + return [`manually`]; + } + return value; } }, { - key: `assignment_poll_default_group_ids`, + key: `assignment_poll_setting_group_ids`, subscriptionKey: { idField: `assignment_poll_config_id`, field: `group_ids` @@ -1057,87 +1084,100 @@ export const meetingSettings: SettingsGroup[] = fillInSettingsDefaults([ type: `groups` }, { - key: `assignment_poll_default_visibility`, + key: `assignment_poll_setting_method`, subscriptionKey: { idField: `assignment_poll_config_id`, - field: `visibility` + field: `method` }, - label: _(`Default voting type`), + label: _(`Default assignment voting method`), type: `choice`, - choiceI18nPrefix: 'poll_visibility', - choices: ['manually', 'named', 'open', 'secret'], - restrictionFn: (orgaSettings, value: any): any => { - const isElectronicVotingEnabled = orgaSettings.instant(`enable_electronic_voting`); - if (!isElectronicVotingEnabled && typeof value !== `string`) { - return [`manually`]; - } - return value; + choices: { + 'selection.yes': _(`Yes per candidate`), + 'selection.no': _(`No per candidate`), + rating_score: _(`Amount per candidate`), + 'rating_approval.yes_no': _(`Yes/No per candidate`), + 'rating_approval.yes_no_abstain': _(`Yes/No/Abstain per candidate`), + 'approval.yes_no': _(`Yes/No per list`), + 'approval.yes_no_abstain': _(`Yes/No/Abstain per list`) } }, { - key: `assignment_poll_default_allow_abstain`, + key: `assignment_poll_setting_enable_max_yes_votes`, subscriptionKey: { idField: `assignment_poll_config_id`, - field: `allow_abstain` + field: `enable_max_yes_votes` }, - label: _(`Use poll method Yes/No/Abstain by default`), + label: _(`Allow limiting the amount of yes votes in per candidate polls`), type: `boolean` - // TODO: Disable depending on vote method }, { - key: `assignment_poll_default_allow_nota`, + key: `assignment_poll_setting_enable_cumulative_voting`, subscriptionKey: { idField: `assignment_poll_config_id`, - field: `allow_nota` + field: `enable_cumulative_voting` }, - label: _(`Allow general option by default`), + label: _(`Allow to accumulate several votes on one option ("cumulative voting")`), type: `boolean` - // TODO: Disable depending on vote method }, { - key: `assignment_poll_default_strike_out`, + key: `assignment_poll_setting_enable_max_options_limit`, + subscriptionKey: { + idField: `assignment_poll_config_id`, + field: `enable_max_options_limit` + }, + label: _(`Allow to limit options that can be voted`), + type: `boolean`, + indentation: 1 + }, + { + key: `assignment_poll_setting_allow_live_voting`, subscriptionKey: { idField: `assignment_poll_config_id`, - field: `strike_out` + field: `allow_live_voting` }, - label: _(`Use disapproval voting by default`), + label: _(`Activate live voting`), type: `boolean` - // TODO: Disable depending on vote method }, { - key: `assignment_poll_default_onehundred_percent_base`, + key: `assignment_poll_setting_enable_live_voting`, subscriptionKey: { idField: `assignment_poll_config_id`, - field: `onehundred_percent_base` + field: `enable_live_voting` }, - label: _(`Default 100 % base`), + label: _(`Set live voting as default`), + type: `boolean`, + indentation: 1 + }, + { + key: `assignment_poll_setting_required_majority`, + subscriptionKey: { + idField: `assignment_poll_config_id`, + field: `required_majority` + }, + label: _(`Default required majority`), type: `choice`, - choiceI18nPrefix: 'poll_percent_base', - choices: ['yes_no', 'valid', 'cast' /* , 'entitled', 'entitled_present' */, 'disabled'] + choiceI18nPrefix: 'poll_required_majority', + choices: ['no_majority', 'simple_majority', 'absolute_majority', 'two_third_majority'] }, { - key: `assignment_poll_default_display_chart`, + key: `assignment_poll_setting_onehundred_percent_base`, subscriptionKey: { idField: `assignment_poll_config_id`, - field: `display_chart` + field: `onehundred_percent_base` }, - label: _(`Default poll chart type`), + label: _(`Default 100 % base`), type: `choice`, - choices: { - table: _(`Table`), - pie: _(`Pie chart`) - } - // TODO: Disable depending on vote method + choiceI18nPrefix: 'poll_percent_base', + choices: ['yes_no', 'valid', 'cast', 'no_general', 'entitled', 'entitled_present', 'disabled'] }, { - key: `assignment_poll_default_sort_result_by_votes`, + key: `assignment_poll_setting_sort_result_by_votes`, subscriptionKey: { idField: `assignment_poll_config_id`, field: `sort_result_by_votes` }, label: _(`Sort election results by amount of votes`), type: `boolean` - // TODO: Disable depending on vote method } ] } diff --git a/client/src/app/site/pages/meetings/view-models/view-meeting-poll-default.ts b/client/src/app/site/pages/meetings/view-models/view-meeting-poll-default.ts deleted file mode 100644 index 8e92e88ee9..0000000000 --- a/client/src/app/site/pages/meetings/view-models/view-meeting-poll-default.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { MeetingPollDefault } from '@app/domain/models/meetings/meeting-poll-default'; -import { BaseViewModel, ViewModelRelations } from '@app/site/base/base-view-model'; - -import { ViewGroup } from '../pages/participants/modules/groups/view-models/view-group'; -import { ViewMeeting } from './view-meeting'; - -export class ViewMeetingPollDefault extends BaseViewModel { - public static COLLECTION = MeetingPollDefault.COLLECTION; - - protected _collection = MeetingPollDefault.COLLECTION; -} -interface IMeetingPollDefaultRelations { - meeting: ViewMeeting; - groups: ViewGroup[]; -} - -export interface ViewMeetingPollDefault extends MeetingPollDefault, ViewModelRelations {} diff --git a/client/src/app/site/pages/meetings/view-models/view-meeting-poll-setting.ts b/client/src/app/site/pages/meetings/view-models/view-meeting-poll-setting.ts new file mode 100644 index 0000000000..f19e6b2fa9 --- /dev/null +++ b/client/src/app/site/pages/meetings/view-models/view-meeting-poll-setting.ts @@ -0,0 +1,17 @@ +import { MeetingPollSetting } from '@app/domain/models/meetings/meeting-poll-setting'; +import { BaseViewModel, ViewModelRelations } from '@app/site/base/base-view-model'; + +import { ViewGroup } from '../pages/participants/modules/groups/view-models/view-group'; +import { ViewMeeting } from './view-meeting'; + +export class ViewMeetingPollSetting extends BaseViewModel { + public static COLLECTION = MeetingPollSetting.COLLECTION; + + protected _collection = MeetingPollSetting.COLLECTION; +} +interface IMeetingPollDefaultRelations { + meeting: ViewMeeting; + groups: ViewGroup[]; +} + +export interface ViewMeetingPollSetting extends MeetingPollSetting, ViewModelRelations {} diff --git a/client/src/app/site/pages/meetings/view-models/view-meeting.ts b/client/src/app/site/pages/meetings/view-models/view-meeting.ts index 27e1a42369..9fb0fd6b40 100644 --- a/client/src/app/site/pages/meetings/view-models/view-meeting.ts +++ b/client/src/app/site/pages/meetings/view-models/view-meeting.ts @@ -1,4 +1,3 @@ -import { Id } from '@app/domain/definitions/key-types'; import { HasProjectorTitle } from '@app/domain/interfaces/has-projector-title'; import { HasProperties } from '@app/domain/interfaces/has-properties'; import { FONT_PLACES, FontPlace, LOGO_PLACES, LogoPlace } from '@app/domain/models/mediafiles/mediafile.constants'; @@ -7,7 +6,7 @@ import { ViewMeetingDefaultProjectorsKey, ViewMeetingMediafileUsageKey } from '@app/domain/models/meetings/meeting.constants'; -import { MeetingPollDefault } from '@app/domain/models/meetings/meeting-poll-default'; +import { MeetingPollSetting } from '@app/domain/models/meetings/meeting-poll-setting'; import { ProjectiondefaultValue } from '@app/domain/models/projector/projection-default'; import { ViewHistoryEntry } from '@app/gateways/repositories/history-entry/view-history-entry'; import { ViewModelRelations } from '@app/site/base/base-view-model'; @@ -50,7 +49,7 @@ import { ViewProjection } from '../pages/projectors/view-models/view-projection' import { ViewProjector } from '../pages/projectors/view-models/view-projector'; import { ViewProjectorCountdown } from '../pages/projectors/view-models/view-projector-countdown'; import { ViewProjectorMessage } from '../pages/projectors/view-models/view-projector-message'; -import { ViewMeetingPollDefault } from './view-meeting-poll-default'; +import { ViewMeetingPollSetting } from './view-meeting-poll-setting'; import { ViewUser } from './view-user'; export const MEETING_LIST_SUBSCRIPTION = `meeting_list`; @@ -101,83 +100,119 @@ export class ViewMeeting extends BaseHasMeetingUsersViewModel { } // Poll defaults aliases - public get topic_poll_default_group_ids(): Id[] { - return this.topic_poll_config.group_ids; + public get topic_poll_setting_enable_cumulative_voting(): MeetingPollSetting[`enable_cumulative_voting`] { + return this.topic_poll_config.enable_cumulative_voting; } - public get assignment_poll_default_group_ids(): Id[] { - return this.assignment_poll_config.group_ids; + public get topic_poll_setting_allow_live_voting(): MeetingPollSetting[`allow_live_voting`] { + return this.topic_poll_config.allow_live_voting; } - public get motion_poll_default_group_ids(): Id[] { - return this.motion_poll_config.group_ids; + public get topic_poll_setting_visibility(): MeetingPollSetting[`visibility`] { + return this.topic_poll_config.visibility; } - public get topic_poll_default_visibility(): MeetingPollDefault[`visibility`] { - return this.topic_poll_config.visibility; + public get topic_poll_setting_method(): MeetingPollSetting[`method`] { + return this.topic_poll_config.method; } - public get topic_poll_default_allow_abstain(): MeetingPollDefault[`allow_abstain`] { - return this.topic_poll_config.allow_abstain; + public get topic_poll_setting_group_ids(): MeetingPollSetting[`group_ids`] { + return this.topic_poll_config.group_ids; } - public get topic_poll_default_allow_nota(): MeetingPollDefault[`allow_nota`] { - return this.topic_poll_config.allow_nota; + public get topic_poll_setting_enable_max_yes_votes(): MeetingPollSetting[`enable_max_yes_votes`] { + return this.topic_poll_config.enable_max_yes_votes; } - public get topic_poll_default_strike_out(): MeetingPollDefault[`strike_out`] { - return this.topic_poll_config.strike_out; + public get topic_poll_setting_enable_max_options_limit(): MeetingPollSetting[`enable_max_options_limit`] { + return this.topic_poll_config.enable_max_options_limit; } - public get topic_poll_default_onehundred_percent_base(): MeetingPollDefault[`onehundred_percent_base`] { - return this.topic_poll_config.onehundred_percent_base; + public get topic_poll_setting_enable_live_voting(): MeetingPollSetting[`enable_live_voting`] { + return this.topic_poll_config.enable_live_voting; } - public get topic_poll_default_display_chart(): MeetingPollDefault[`display_chart`] { - return this.topic_poll_config.display_chart; + public get topic_poll_setting_required_majority(): MeetingPollSetting[`required_majority`] { + return this.topic_poll_config.required_majority; } - public get topic_poll_default_sort_result_by_votes(): MeetingPollDefault[`sort_result_by_votes`] { + public get topic_poll_setting_onehundred_percent_base(): MeetingPollSetting[`onehundred_percent_base`] { + return this.topic_poll_config.onehundred_percent_base; + } + + public get topic_poll_setting_sort_result_by_votes(): MeetingPollSetting[`sort_result_by_votes`] { return this.topic_poll_config.sort_result_by_votes; } - public get motion_poll_default_visibility(): MeetingPollDefault[`visibility`] { + public get motion_poll_setting_visibility(): MeetingPollSetting[`visibility`] { return this.motion_poll_config.visibility; } - public get motion_poll_default_allow_abstain(): MeetingPollDefault[`allow_abstain`] { - return this.motion_poll_config.allow_abstain; + public get motion_poll_setting_group_ids(): MeetingPollSetting[`group_ids`] { + return this.motion_poll_config.group_ids; + } + + public get motion_poll_setting_method(): MeetingPollSetting[`method`] { + return this.motion_poll_config.method; + } + + public get motion_poll_setting_allow_live_voting(): MeetingPollSetting[`allow_live_voting`] { + return this.motion_poll_config.allow_live_voting; } - public get motion_poll_default_onehundred_percent_base(): MeetingPollDefault[`onehundred_percent_base`] { + public get motion_poll_setting_enable_live_voting(): MeetingPollSetting[`enable_live_voting`] { + return this.motion_poll_config.enable_live_voting; + } + + public get motion_poll_setting_required_majority(): MeetingPollSetting[`required_majority`] { + return this.motion_poll_config.required_majority; + } + + public get motion_poll_setting_onehundred_percent_base(): MeetingPollSetting[`onehundred_percent_base`] { return this.motion_poll_config.onehundred_percent_base; } - public get assignment_poll_default_visibility(): MeetingPollDefault[`visibility`] { + public get assignment_poll_setting_visibility(): MeetingPollSetting[`visibility`] { return this.assignment_poll_config.visibility; } - public get assignment_poll_default_allow_abstain(): MeetingPollDefault[`allow_abstain`] { - return this.assignment_poll_config.allow_abstain; + public get assignment_poll_setting_group_ids(): MeetingPollSetting[`group_ids`] { + return this.assignment_poll_config.group_ids; } - public get assignment_poll_default_allow_nota(): MeetingPollDefault[`allow_nota`] { - return this.assignment_poll_config.allow_nota; + public get assignment_poll_setting_method(): MeetingPollSetting[`method`] { + return this.assignment_poll_config.method; } - public get assignment_poll_default_strike_out(): MeetingPollDefault[`strike_out`] { - return this.assignment_poll_config.strike_out; + public get assignment_poll_setting_enable_max_yes_votes(): MeetingPollSetting[`enable_max_yes_votes`] { + return this.assignment_poll_config.enable_max_yes_votes; } - public get assignment_poll_default_onehundred_percent_base(): MeetingPollDefault[`onehundred_percent_base`] { - return this.assignment_poll_config.onehundred_percent_base; + public get assignment_poll_setting_enable_cumulative_voting(): MeetingPollSetting[`enable_cumulative_voting`] { + return this.assignment_poll_config.enable_cumulative_voting; + } + + public get assignment_poll_setting_enable_max_options_limit(): MeetingPollSetting[`enable_max_options_limit`] { + return this.assignment_poll_config.enable_max_options_limit; } - public get assignment_poll_default_display_chart(): MeetingPollDefault[`display_chart`] { - return this.assignment_poll_config.display_chart; + public get assignment_poll_setting_allow_live_voting(): MeetingPollSetting[`allow_live_voting`] { + return this.assignment_poll_config.allow_live_voting; + } + + public get assignment_poll_setting_enable_live_voting(): MeetingPollSetting[`enable_live_voting`] { + return this.assignment_poll_config.enable_live_voting; + } + + public get assignment_poll_setting_required_majority(): MeetingPollSetting[`required_majority`] { + return this.assignment_poll_config.required_majority; + } + + public get assignment_poll_setting_onehundred_percent_base(): MeetingPollSetting[`onehundred_percent_base`] { + return this.assignment_poll_config.onehundred_percent_base; } - public get assignment_poll_default_sort_result_by_votes(): MeetingPollDefault[`sort_result_by_votes`] { + public get assignment_poll_setting_sort_result_by_votes(): MeetingPollSetting[`sort_result_by_votes`] { return this.assignment_poll_config.sort_result_by_votes; } @@ -304,9 +339,9 @@ interface IMeetingRelations { point_of_order_categories: ViewPointOfOrderCategory[]; structure_levels: ViewStructureLevel[]; relevant_history_entries: ViewHistoryEntry[]; - assignment_poll_config: ViewMeetingPollDefault; - motion_poll_config: ViewMeetingPollDefault; - topic_poll_config: ViewMeetingPollDefault; + assignment_poll_config: ViewMeetingPollSetting; + motion_poll_config: ViewMeetingPollSetting; + topic_poll_config: ViewMeetingPollSetting; } export interface ViewMeeting extends diff --git a/meta b/meta index a11c29cebe..1a8e351979 160000 --- a/meta +++ b/meta @@ -1 +1 @@ -Subproject commit a11c29cebe12c4fcce9ccb78825e7e5b6d1aa743 +Subproject commit 1a8e3519793ad72e6e142857098bf67ec583dba9 From a2687fdcbcd6fc869a6935be6cde9cac80e901b2 Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Fri, 16 Oct 2026 11:12:51 +0200 Subject: [PATCH 19/26] Fix autopilot titles for topics sometimes not displayed --- .../autopilot/components/autopilot/autopilot.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/app/site/pages/meetings/pages/autopilot/components/autopilot/autopilot.component.ts b/client/src/app/site/pages/meetings/pages/autopilot/components/autopilot/autopilot.component.ts index 10a4095871..26b61b86e1 100644 --- a/client/src/app/site/pages/meetings/pages/autopilot/components/autopilot/autopilot.component.ts +++ b/client/src/app/site/pages/meetings/pages/autopilot/components/autopilot/autopilot.component.ts @@ -145,7 +145,8 @@ export class AutopilotComponent extends BaseMeetingComponent implements OnInit { this.projector = refProjector; const currentProjections = refProjector.nonStableCurrentProjections; this._currentProjection = - currentProjections.length > 0 && !!currentProjections[0].meeting_id + currentProjections.length > 0 && + (!!currentProjections[0].content_object_id || !!currentProjections[0].meeting_id) ? currentProjections[0] : null; this.projectedViewModel = this._currentProjection?.content_object || null; From 3098a1bbdabb1974dc050fc0fb8f5ea43f2728e5 Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Fri, 16 Oct 2026 14:19:58 +0200 Subject: [PATCH 20/26] Fix incomplete model updates --- .../gateways/repositories/base-repository.ts | 17 ++++++++++++----- .../services/autoupdate/autoupdate.service.ts | 5 +++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/client/src/app/gateways/repositories/base-repository.ts b/client/src/app/gateways/repositories/base-repository.ts index ab426368c5..b3dff6a252 100644 --- a/client/src/app/gateways/repositories/base-repository.ts +++ b/client/src/app/gateways/repositories/base-repository.ts @@ -157,11 +157,18 @@ export abstract class BaseRepository { - if (models) { - this.updateViewModelListSubject(models); - } - }); + this.unsafeViewModelListSubject + .pipe( + throttleTime(20, undefined, { + leading: true, + trailing: true + }) + ) + .subscribe(models => { + if (models) { + this.updateViewModelListSubject(models); + } + }); this.languageCollator = getIntlCollatorForLang(this.translate.getCurrentLang() ?? `en`); } diff --git a/client/src/app/site/services/autoupdate/autoupdate.service.ts b/client/src/app/site/services/autoupdate/autoupdate.service.ts index 4d8ea1b5c5..47dc1783f8 100644 --- a/client/src/app/site/services/autoupdate/autoupdate.service.ts +++ b/client/src/app/site/services/autoupdate/autoupdate.service.ts @@ -333,8 +333,9 @@ export class AutoupdateService { delete this._resolveDataReceived[requestId]; } } + }) + .finally(() => { + unlock(); }); - - unlock(); } } From d498a83ed17ac5972aacd3719a2abb36eb29d3b3 Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Fri, 16 Oct 2026 14:55:56 +0200 Subject: [PATCH 21/26] Fix no general 100% base --- .../meetings/pages/polls/view-models/poll-config-selection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-selection.ts b/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-selection.ts index ec0d92fb53..ff538537fb 100644 --- a/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-selection.ts +++ b/client/src/app/site/pages/meetings/pages/polls/view-models/poll-config-selection.ts @@ -62,7 +62,7 @@ export class ViewPollConfigSelection extends BasePollConfigViewModel Date: Fri, 16 Oct 2026 15:04:26 +0200 Subject: [PATCH 22/26] Prevent edit of published poll result --- .../topic-poll-dialog/topic-poll-dialog.component.html | 2 +- .../assignment-poll-dialog.component.html | 2 +- .../motion-poll-dialog/motion-poll-dialog.component.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 36890386d5..79b7ac8670 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 @@ -40,7 +40,7 @@ - } @else { + } @else if (!pollData.published) { diff --git a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html index 1957f376b6..008b0b70bd 100644 --- a/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html +++ b/client/src/app/site/pages/meetings/pages/assignments/modules/assignment-poll/components/assignment-poll-dialog/assignment-poll-dialog.component.html @@ -40,7 +40,7 @@ - } @else { + } @else if (!pollData.published) { diff --git a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.html b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.html index 8c24e0427c..50df34dd7e 100644 --- a/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.html +++ b/client/src/app/site/pages/meetings/pages/motions/modules/motion-poll/components/motion-poll-dialog/motion-poll-dialog.component.html @@ -40,7 +40,7 @@ - } @else { + } @else if (!pollData.published) { From c79556d5ba71c76b53bb1b2ae9b326d24220e396 Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Fri, 16 Oct 2026 21:48:33 +0200 Subject: [PATCH 23/26] Improve poll method selection on optionEdit --- .../poll/components/poll-form/poll-form.component.html | 8 ++++---- .../poll/components/poll-form/poll-form.component.ts | 6 +++++- 2 files changed, 9 insertions(+), 5 deletions(-) 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 fb6ad7cc06..1ce75004f4 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 @@ -61,14 +61,14 @@

{{ 'Election method' | translate }} - @if (optionAmount() <= 1 && form.options().value().length <= 1) { + @if (optionEdit() || (optionAmount() <= 1 && form.options().value().length <= 1)) { {{ 'Yes/No' | translate }} {{ 'Yes/No/Abstain' | translate }} } - @if (optionAmount() > 1 || form.options().value().length > 1) { + @if (optionEdit() || (optionAmount() > 1 || form.options().value().length > 1)) { @if (optionType() === 'text') { {{ 'Yes per option' | translate }} @@ -118,7 +118,7 @@

- @switch (this.selectedMethod()) { + @switch (selectedMethod()) { @case ('approval') { } @@ -149,7 +149,7 @@

- @if (optionEdit()) { + @if (optionEdit() && selectedMethod() !== `approval`) {

{{ 'Options' | translate }}

(() => { - return this.form().valid() && this.methodForm()?.formValid(); + return ( + this.form().valid() && + this.methodForm()?.formValid() && + (this.selectedMethod() === `approval` || this.optionAmount() > 0 || this.form.options().value()?.length > 0) + ); }); public isCreated = computed(() => { From 27d926d0515dc9c98b181d88648c03ca89df3f4e Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Fri, 16 Oct 2026 21:51:42 +0200 Subject: [PATCH 24/26] Check config resource availability in poll meta --- .../poll-meta/poll-meta.component.html | 159 +++++++++--------- .../poll-meta/poll-meta.component.ts | 4 +- 2 files changed, 83 insertions(+), 80 deletions(-) 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 3bc16aae68..f8e30e3c86 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 @@ -52,11 +52,12 @@
- @if (poll().entitled_groups && poll().isEVoting) { + @let entitledGroups = poll().entitled_groups$ | async; + @if (entitledGroups && poll().isEVoting) {
{{ 'Entitled to vote' | translate }}: @if (poll().entitled_groups.length) { - + {{ group.getTitle() }} } @else { @@ -65,102 +66,104 @@
} - @if (config.value().strike_out !== undefined || config.value().allow_abstain !== undefined) { -
- {{ 'Voting method' | translate }}: - @if (config.value().strike_out === true) { - @if (isPersonPoll()) { - {{ 'No per candidate' | translate }} - } @else { - {{ 'No per option' | translate }} - } - } @else if (config.value().strike_out === false) { - @if (isPersonPoll()) { - {{ 'Yes per candidate' | translate }} - } @else { - {{ 'Yes per option' | translate }} + @if (config.hasValue() && config.value()) { + @if (config.value().strike_out !== undefined || config.value().allow_abstain !== undefined) { +
+ {{ 'Voting method' | translate }}: + @if (config.value().strike_out === true) { + @if (isPersonPoll()) { + {{ 'No per candidate' | translate }} + } @else { + {{ 'No per option' | translate }} + } + } @else if (config.value().strike_out === false) { + @if (isPersonPoll()) { + {{ 'Yes per candidate' | translate }} + } @else { + {{ 'Yes per option' | translate }} + } + } @else if (config.value().allow_abstain === true) { + {{ 'Yes/No/Abstain' | translate }} + } @else if (config.value().allow_abstain === false) { + {{ 'Yes/No' | translate }} } - } @else if (config.value().allow_abstain === true) { - {{ 'Yes/No/Abstain' | translate }} - } @else if (config.value().allow_abstain === false) { - {{ 'Yes/No' | translate }} - } -
- } +
+ } - @if (config.value().COLLECTION === `poll_config_selection`) { - @if (config.value().min_options_amount > 1 || config.value().max_options_amount > 1) { - @if (config.value().min_options_amount === config.value().max_options_amount) { -
- {{ 'Votes on different options' | translate }}: - {{ config.value().min_options_amount }} -
- } @else { - @if (config.value().min_options_amount > 1) { + @if (config.value().COLLECTION === `poll_config_selection`) { + @if (config.value().min_options_amount > 1 || config.value().max_options_amount > 1) { + @if (config.value().min_options_amount === config.value().max_options_amount) {
- {{ 'Minimum amount of options selected' | translate }}: + {{ 'Votes on different options' | translate }}: {{ config.value().min_options_amount }}
- } + } @else { + @if (config.value().min_options_amount > 1) { +
+ {{ 'Minimum amount of options selected' | translate }}: + {{ config.value().min_options_amount }} +
+ } - @if (config.value().max_options_amount > 1) { -
- {{ 'Maximum amount options selected' | translate }}: - {{ config.value().max_options_amount }} -
+ @if (config.value().max_options_amount > 1) { +
+ {{ 'Maximum amount options selected' | translate }}: + {{ config.value().max_options_amount }} +
+ } } } - } - } @else { - @if (config.value().min_vote_sum > 1 || config.value().max_vote_sum > 1) { - @if (config.value().min_vote_sum === config.value().max_vote_sum) { -
- {{ 'Votes' | translate }}: - {{ config.value().min_vote_sum }} -
- } @else { - @if (config.value().min_vote_sum > 1) { + } @else { + @if (config.value().min_vote_sum > 1 || config.value().max_vote_sum > 1) { + @if (config.value().min_vote_sum === config.value().max_vote_sum) {
- {{ 'Minimum amount of votes' | translate }}: + {{ 'Votes' | translate }}: {{ config.value().min_vote_sum }}
- } + } @else { + @if (config.value().min_vote_sum > 1) { +
+ {{ 'Minimum amount of votes' | translate }}: + {{ config.value().min_vote_sum }} +
+ } - @if (config.value().max_vote_sum > 1) { -
- {{ 'Maximum amount of votes' | translate }}: - {{ config.value().max_vote_sum }} -
+ @if (config.value().max_vote_sum > 1) { +
+ {{ 'Maximum amount of votes' | translate }}: + {{ config.value().max_vote_sum }} +
+ } } } + + @if (config.value().max_yes_amount >= 1 && config.value().max_yes_amount < config.value().max_vote_sum) { +
+ {{ 'Maximum amount of yes votes' | translate }}: + {{ config.value().max_yes_amount }} +
+ } } - @if (config.value().max_yes_amount >= 1 && config.value().max_yes_amount < config.value().max_vote_sum) { + @if (config.value().max_votes_per_option > 1) {
- {{ 'Maximum amount of yes votes' | translate }}: - {{ config.value().max_yes_amount }} + {{ 'Maximum amount of votes per option' | translate }}: + {{ config.value().max_votes_per_option }}
} - } - @if (config.value().max_votes_per_option > 1) { -
- {{ 'Maximum amount of votes per option' | translate }}: - {{ config.value().max_votes_per_option }} -
- } - - @if (config.value().onehundred_percent_base !== undefined) { -
- {{ '100% base' | translate }}: - {{ config.value().onehundred_percent_base | translateKey: 'poll_percent_base' }} -
- } + @if (config.value().onehundred_percent_base !== undefined) { +
+ {{ '100% base' | translate }}: + {{ config.value().onehundred_percent_base | translateKey: 'poll_percent_base' }} +
+ } - @if (config.value().required_majority !== undefined && config.value().required_majority !== 'no_majority') { -
- {{ 'Required majority' | translate }}: - {{ config.value().required_majority | translateKey: 'poll_required_majority' }} -
+ @if (config.value().required_majority !== undefined && config.value().required_majority !== 'no_majority') { +
+ {{ 'Required majority' | translate }}: + {{ config.value().required_majority | translateKey: 'poll_required_majority' }} +
+ } }
diff --git a/client/src/app/site/pages/meetings/modules/poll/components/poll-meta/poll-meta.component.ts b/client/src/app/site/pages/meetings/modules/poll/components/poll-meta/poll-meta.component.ts index 92f60de2b0..6a9e66135f 100644 --- a/client/src/app/site/pages/meetings/modules/poll/components/poll-meta/poll-meta.component.ts +++ b/client/src/app/site/pages/meetings/modules/poll/components/poll-meta/poll-meta.component.ts @@ -1,4 +1,4 @@ -import { NgTemplateOutlet } from '@angular/common'; +import { AsyncPipe, NgTemplateOutlet } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; import { rxResource } from '@angular/core/rxjs-interop'; import { PollConfigApproval } from '@app/domain/models/poll/poll-config-approval'; @@ -18,7 +18,7 @@ import { ViewPollOption } from '../../../../pages/polls/view-models/poll-option' @Component({ selector: 'os-poll-meta', - imports: [CommaSeparatedListingComponent, TranslatePipe, TranslateKeyPipe, NgTemplateOutlet], + imports: [CommaSeparatedListingComponent, TranslatePipe, TranslateKeyPipe, NgTemplateOutlet, AsyncPipe], templateUrl: './poll-meta.component.html', styleUrl: './poll-meta.component.scss', changeDetection: ChangeDetectionStrategy.OnPush From e70e815931204eec5a9f10d375aa1e8d32af5b22 Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Fri, 16 Oct 2026 21:57:01 +0200 Subject: [PATCH 25/26] Fix console error in poll-single-votes --- .../poll-single-votes/poll-single-votes.component.html | 2 +- .../components/poll-single-votes/poll-single-votes.component.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) 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 e48a4bb8f1..c92f0d1573 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,6 +1,6 @@
Date: Fri, 16 Oct 2026 22:20:34 +0200 Subject: [PATCH 26/26] Hide option edit after poll is started --- .../modules/poll/components/poll-form/poll-form.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1ce75004f4..cd645ab1ca 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 @@ -149,7 +149,7 @@

- @if (optionEdit() && selectedMethod() !== `approval`) { + @if (optionEdit() && selectedMethod() !== `approval` && isCreated()) {

{{ 'Options' | translate }}