From fff1f15b6c0adfb95d76723c9733c0aa3357f3e3 Mon Sep 17 00:00:00 2001 From: GabrielMeyer Date: Wed, 15 Jan 2020 15:12:33 +0100 Subject: [PATCH] Polls for motions and assignments - Adds charts to assignments - Creates base-classes for polls --- .../components/banner/banner.component.html | 3 +- .../components/banner/banner.component.scss | 3 +- .../banner/banner.component.scss-theme.scss | 11 + .../components/charts/charts.component.html | 44 ++-- .../components/charts/charts.component.ts | 99 ++++++-- .../assignment-detail.component.ts | 1 + .../assignment-poll-detail.component.html | 51 +++- .../assignment-poll-detail.component.ts | 29 ++- .../assignment-poll-vote.component.html | 19 +- .../assignment-poll.component.html | 223 ++---------------- .../assignment-poll.component.ts | 27 ++- .../models/view-assignment-poll.ts | 48 +++- .../site/motions/models/view-motion-poll.ts | 23 +- .../motion-detail.component.html | 6 +- .../motion-poll-detail.component.html | 65 +++-- .../motion-poll-detail.component.scss | 7 +- .../motion-poll-detail.component.ts | 44 +++- .../motion-poll-dialog.component.html | 6 +- .../motion-poll-dialog.component.ts | 17 +- .../motion-poll/motion-poll.component.html | 17 +- .../motion-poll/motion-poll.component.ts | 58 ++++- .../motions/services/motion-poll.service.ts | 2 +- .../components/base-poll-detail.component.ts | 59 +++-- .../polls/components/base-poll.component.ts | 34 ++- .../poll-form/poll-form.component.html | 9 +- .../poll-form/poll-form.component.ts | 33 ++- .../app/site/polls/models/view-base-poll.ts | 16 ++ client/src/styles.scss | 2 + 28 files changed, 570 insertions(+), 386 deletions(-) create mode 100644 client/src/app/shared/components/banner/banner.component.scss-theme.scss diff --git a/client/src/app/shared/components/banner/banner.component.html b/client/src/app/shared/components/banner/banner.component.html index 6b86d697b..9511f1074 100644 --- a/client/src/app/shared/components/banner/banner.component.html +++ b/client/src/app/shared/components/banner/banner.component.html @@ -1,4 +1,5 @@ - diff --git a/client/src/app/shared/components/charts/charts.component.ts b/client/src/app/shared/components/charts/charts.component.ts index 5aefc5421..a9030fa6e 100644 --- a/client/src/app/shared/components/charts/charts.component.ts +++ b/client/src/app/shared/components/charts/charts.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core'; import { MatSnackBar } from '@angular/material'; import { Title } from '@angular/platform-browser'; @@ -12,7 +12,7 @@ import { BaseViewComponent } from 'app/site/base/base-view'; /** * The different supported chart-types. */ -export type ChartType = 'line' | 'bar' | 'pie' | 'doughnut' | 'horizontalBar'; +export type ChartType = 'line' | 'bar' | 'pie' | 'doughnut' | 'horizontalBar' | 'stackedBar'; /** * Describes the events the chart is fired, when hovering or clicking on it. @@ -30,6 +30,8 @@ export interface ChartDate { label: string; backgroundColor?: string; hoverBackgroundColor?: string; + barThickness?: number; + maxBarThickness?: number; } /** @@ -37,6 +39,8 @@ export interface ChartDate { */ export type ChartData = ChartDate[]; +export type ChartLegendSize = 'small' | 'middle'; + /** * Wrapper for the chart-library. * @@ -45,7 +49,8 @@ export type ChartData = ChartDate[]; @Component({ selector: 'os-charts', templateUrl: './charts.component.html', - styleUrls: ['./charts.component.scss'] + styleUrls: ['./charts.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class ChartsComponent extends BaseViewComponent { /** @@ -57,15 +62,22 @@ export class ChartsComponent extends BaseViewComponent { public set data(dataObservable: Observable) { this.subscriptions.push( dataObservable.subscribe(data => { + if (!data) { + return; + } + data = data.flatMap((date: ChartDate) => ({ ...date, data: date.data.filter(value => value >= 0) })); this.chartData = data; this.circleData = data.flatMap((date: ChartDate) => date.data); this.circleLabels = data.map(date => date.label); - this.circleColors = [ + const circleColors = [ { - backgroundColor: data.map(date => date.backgroundColor), - hoverBackgroundColor: data.map(date => date.hoverBackgroundColor) + backgroundColor: data.map(date => date.backgroundColor).filter(color => !!color), + hoverBackgroundColor: data.map(date => date.hoverBackgroundColor).filter(color => !!color) } ]; + this.circleColors = !!circleColors[0].backgroundColor.length ? circleColors : null; + this.checkChartType(); + this.cd.detectChanges(); }) ); } @@ -75,16 +87,20 @@ export class ChartsComponent extends BaseViewComponent { */ @Input() public set type(type: ChartType) { - if (type === 'horizontalBar') { - this.setupHorizontalBar(); - } - this._type = type; + this.checkChartType(type); + this.cd.detectChanges(); } public get type(): ChartType { return this._type; } + @Input() + public set chartLegendSize(size: ChartLegendSize) { + this._chartLegendSize = size; + this.setupChartLegendSize(); + } + /** * Whether to show the legend. */ @@ -147,11 +163,12 @@ export class ChartsComponent extends BaseViewComponent { responsive: true, legend: { position: 'top', - labels: { - fontSize: 14 - } + labels: {} + }, + scales: { + xAxes: [{ ticks: { beginAtZero: true } }], + yAxes: [{ ticks: { beginAtZero: true } }] }, - scales: { xAxes: [{}], yAxes: [{ ticks: { beginAtZero: true } }] }, plugins: { datalabels: { anchor: 'end', @@ -165,6 +182,8 @@ export class ChartsComponent extends BaseViewComponent { */ private _type: ChartType = 'bar'; + private _chartLegendSize: ChartLegendSize = 'middle'; + /** * Constructor. * @@ -173,17 +192,63 @@ export class ChartsComponent extends BaseViewComponent { * @param matSnackbar * @param cd */ - public constructor(title: Title, protected translate: TranslateService, matSnackbar: MatSnackBar) { + public constructor( + title: Title, + protected translate: TranslateService, + matSnackbar: MatSnackBar, + private cd: ChangeDetectorRef + ) { super(title, translate, matSnackbar); } /** - * Changes the chart-options, if the `horizontalBar` is used. + * Changes the chart-options, if the `stackedBar` is used. */ - private setupHorizontalBar(): void { + private setupStackedBar(): void { this.chartOptions.scales = Object.assign(this.chartOptions.scales, { xAxes: [{ stacked: true }], yAxes: [{ stacked: true }] }); } + + private setupBar(): void { + if (!this.chartData.every(date => date.barThickness && date.maxBarThickness)) { + this.chartData = this.chartData.map(chartDate => ({ + ...chartDate, + barThickness: 20, + maxBarThickness: 48 + })); + } + } + + private setupChartLegendSize(): void { + switch (this._chartLegendSize) { + case 'small': + this.chartOptions.legend.labels = Object.assign(this.chartOptions.legend.labels, { + fontSize: 10, + boxWidth: 20 + }); + break; + case 'middle': + this.chartOptions.legend.labels = { + fontSize: 14, + boxWidth: 40 + }; + break; + } + this.cd.detectChanges(); + } + + private checkChartType(chartType?: ChartType): void { + let type = chartType || this._type; + if (type === 'stackedBar') { + this.setupStackedBar(); + this.setupBar(); + type = 'horizontalBar'; + } + // if (type === 'bar' || type === 'horizontalBar') { + // this.setupBar(); + // } + this._type = type; + } } diff --git a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts index 5d8e60068..2e329c3d4 100644 --- a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts +++ b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts @@ -374,6 +374,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn // resetting a form triggers a form.next(null) - check if data is present if (formResult && formResult.userId) { this.addUser(formResult.userId); + this.candidatesForm.setValue({ userId: null }); } }) ); diff --git a/client/src/app/site/assignments/components/assignment-poll-detail/assignment-poll-detail.component.html b/client/src/app/site/assignments/components/assignment-poll-detail/assignment-poll-detail.component.html index dd78ffb71..7fcc3ce2c 100644 --- a/client/src/app/site/assignments/components/assignment-poll-detail/assignment-poll-detail.component.html +++ b/client/src/app/site/assignments/components/assignment-poll-detail/assignment-poll-detail.component.html @@ -1,7 +1,7 @@ - +

{{ poll.title }}

@@ -42,23 +42,62 @@

Result

-
+
{{ option.user.full_name }}
-
{{ "Unknown user" | translate}}
+
{{ 'Unknown user' | translate }}
{{ obj.value.user.full_name }}
-
{{ "Unknown user" | translate}}
+
{{ 'Unknown user' | translate }}
-
{{ obj.value.votes[option.user_id]}}
+
{{ obj.value.votes[option.user_id] }}
+
+ + + {{ 'Candidates' | translate }} + {{ row.user }} + + + {{ 'Yes' | translate }} + {{ row.yes }} + + + + {{ 'No' | translate }} + {{ row.no }} + + + + {{ 'Abstain' | translate }} + {{ row.abstain }} + + + + {{ 'Quorum' | translate }} + + + + + + +
diff --git a/client/src/app/site/assignments/components/assignment-poll-detail/assignment-poll-detail.component.ts b/client/src/app/site/assignments/components/assignment-poll-detail/assignment-poll-detail.component.ts index 29d7ed222..384e47093 100644 --- a/client/src/app/site/assignments/components/assignment-poll-detail/assignment-poll-detail.component.ts +++ b/client/src/app/site/assignments/components/assignment-poll-detail/assignment-poll-detail.component.ts @@ -5,9 +5,11 @@ import { ActivatedRoute } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; +import { OperatorService } from 'app/core/core-services/operator.service'; import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service'; import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service'; import { PromptService } from 'app/core/ui-services/prompt.service'; +import { ChartType } from 'app/shared/components/charts/charts.component'; import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll'; import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-detail.component'; import { ViewUser } from 'app/site/users/models/view-user'; @@ -21,8 +23,24 @@ import { ViewAssignmentVote } from '../../models/view-assignment-vote'; styleUrls: ['./assignment-poll-detail.component.scss'] }) export class AssignmentPollDetailComponent extends BasePollDetailComponent { + public isReady = false; + + public candidatesLabels: string[] = []; + public votesByUser: { [key: number]: { user: ViewUser; votes: { [key: number]: ViewAssignmentVote } } }; + public get chartType(): ChartType { + return 'horizontalBar'; + } + + public get columnDefinition(): string[] { + const columns = ['user', 'yes', 'no', 'quorum']; + if ((this.poll).pollmethod === AssignmentPollMethods.YNA) { + columns.splice(3, 0, 'abstain'); + } + return columns; + } + public constructor( title: Title, translate: TranslateService, @@ -31,7 +49,8 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent - {{ "You can distribute" | translate }} {{ poll.votes_amount }} {{ "votes" | translate }}. + {{ 'You can distribute' | translate }} {{ poll.votes_amount }} {{ 'votes' | translate }}.
{{ option.user.getFullName() }} No user {{ option.candidate_id }}
- +
- ({{ "Current" | translate }}: {{ getCurrentVoteVerbose(option.user_id) | translate }}) + ({{ 'Current' | translate }}: {{ getCurrentVoteVerbose(option.user_id) | translate }})
@@ -31,12 +33,17 @@ - +
-
@@ -44,4 +51,4 @@ {{ vmanager.getVotePermissionErrorVerbose(poll) | translate }} -
\ No newline at end of file + diff --git a/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.html b/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.html index b0a7fe7c8..f1543a869 100644 --- a/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.html +++ b/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.html @@ -3,7 +3,7 @@ -
@@ -40,200 +29,38 @@
-
- - - {{ poll.stateVerbose }} - - - -
+
+
+
+ + + {{ poll.stateVerbose }} + +
-

- - {{ poll.title }} - -

+

+ + {{ poll.title }} + +

+
+
+ +
+
- -
-
-
-
-
Candidates
-
Votes
-
-
- Quorum -
-
- - - {{ majorityChoice.display_name | translate }} - - - - - -
-
-
-
-
-
- -
-
- -
- {{ option.user.full_name }} -
- -
-
-
-
- {{ pollService.getLabel(vote.value) | translate }}: - {{ pollService.getSpecialLabel(vote.weight) | translate }} - ({{ pollService.getPercent(poll, option, vote.value) }}%) -
-
- - -
-
-
-
-
-
- {{ pollService.yesQuorum(majorityChoice, poll, option) }} - - {{ pollService.getIcon('yes') }} - {{ pollService.getIcon('no') }} - -
-
-
- -
-
-
-
- {{ pollService.getLabel(key) | translate }}: -
-
- {{ pollService.getSpecialLabel(poll[key]) | translate }} - - ({{ pollService.getValuePercent(poll, key) }} %) - -
-
-
-
-
-

Candidates

-
- {{ option.user.getFullName() }} - No user {{ option.candidate_id }} -
-
-
- - - - - - - - - + - diff --git a/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts b/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts index f2b98c86e..b85b9326c 100644 --- a/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts +++ b/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; @@ -9,7 +9,10 @@ import { TranslateService } from '@ngx-translate/core'; import { OperatorService } from 'app/core/core-services/operator.service'; import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service'; import { PromptService } from 'app/core/ui-services/prompt.service'; +import { ChartType } from 'app/shared/components/charts/charts.component'; +import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll'; import { BasePollComponent } from 'app/site/polls/components/base-poll.component'; +import { PollService } from 'app/site/polls/services/poll.service'; import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service'; import { ViewAssignmentOption } from '../../models/view-assignment-option'; import { ViewAssignmentPoll } from '../../models/view-assignment-poll'; @@ -24,6 +27,27 @@ import { ViewAssignmentPoll } from '../../models/view-assignment-poll'; encapsulation: ViewEncapsulation.None }) export class AssignmentPollComponent extends BasePollComponent implements OnInit { + @Input() + public set poll(value: ViewAssignmentPoll) { + this.initPoll(value); + this.candidatesLabels = value.initChartLabels(); + const chartData = + value.pollmethod === AssignmentPollMethods.Votes + ? value.generateCircleChartData() + : value.generateChartData(); + this.chartDataSubject.next(chartData); + } + + public get poll(): ViewAssignmentPoll { + return this._poll; + } + + public get chartType(): ChartType { + return this.poll && this.poll.pollmethod === AssignmentPollMethods.Votes ? 'doughnut' : 'horizontalBar'; + } + + public candidatesLabels: string[] = []; + /** * Form for updating the poll's description */ @@ -58,6 +82,7 @@ export class AssignmentPollComponent extends BasePollComponent implements public static COLLECTIONSTRING = AssignmentPoll.COLLECTIONSTRING; protected _collectionString = AssignmentPoll.COLLECTIONSTRING; + public readonly tableChartData: Map> = new Map(); public readonly pollClassType: 'assignment' | 'motion' = 'assignment'; + public get pollmethodVerbose(): string { + return AssignmentPollMethodsVerbose[this.pollmethod]; + } + public getSlide(): ProjectorElementBuildDeskriptor { // TODO: update to new voting system? return { @@ -36,13 +44,43 @@ export class ViewAssignmentPoll extends ViewBasePoll implements }; } - public get pollmethodVerbose(): string { - return AssignmentPollMethodsVerbose[this.pollmethod]; + public initChartLabels(): string[] { + return this.options.map(candidate => candidate.user.full_name); } - // TODO public generateChartData(): ChartData { - return []; + const fields = ['yes', 'no']; + if (this.pollmethod === AssignmentPollMethods.YNA) { + fields.push('abstain'); + } + const data: ChartData = fields.map(key => ({ + label: key.toUpperCase(), + data: this.options.map(vote => vote[key]), + backgroundColor: PollColor[key], + hoverBackgroundColor: PollColor[key] + })); + return data; + } + + public generateCircleChartData(): ChartData { + const data: ChartData = this.options.map(candidate => ({ + label: candidate.user.getFullName(), + data: [candidate.yes] + })); + return data; + } + + public generateTableData(): {}[] { + const data = this.options + .map(candidate => ({ + yes: candidate.yes, + no: candidate.no, + abstain: candidate.abstain, + user: candidate.user.full_name + })) + .sort((a, b) => b.yes - a.yes); + + return data; } } diff --git a/client/src/app/site/motions/models/view-motion-poll.ts b/client/src/app/site/motions/models/view-motion-poll.ts index af28f82bd..01e465380 100644 --- a/client/src/app/site/motions/models/view-motion-poll.ts +++ b/client/src/app/site/motions/models/view-motion-poll.ts @@ -20,6 +20,20 @@ export class ViewMotionPoll extends ViewBasePoll implements MotionPo public readonly pollClassType: 'assignment' | 'motion' = 'motion'; + private tableKeys = ['yes', 'no', 'abstain']; + private voteKeys = ['votesvalid', 'votesinvalid', 'votescast']; + + public initChartLabels(): string[] { + return ['Votes']; + } + + public generateTableData(): {}[] { + let tableData = this.options.flatMap(vote => this.tableKeys.map(key => ({ key: key, value: vote[key] }))); + tableData.push(...this.voteKeys.map(key => ({ key: key, value: this[key] }))); + tableData = tableData.map(entry => (entry.value >= 0 ? entry : { key: entry.key, value: null })); + return tableData; + } + public generateChartData(): ChartData { const fields = ['yes', 'no']; if (this.pollmethod === MotionPollMethods.YNA) { @@ -27,18 +41,11 @@ export class ViewMotionPoll extends ViewBasePoll implements MotionPo } const data: ChartData = fields.map(key => ({ label: key.toUpperCase(), - data: [this.options[0][key]], + data: this.options.map(option => option[key]), backgroundColor: PollColor[key], hoverBackgroundColor: PollColor[key] })); - data.push({ - label: 'Votes invalid', - data: [this.votesinvalid], - backgroundColor: PollColor.votesinvalid, - hoverBackgroundColor: PollColor.votesinvalid - }); - return data; } diff --git a/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.html b/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.html index 2a8239e4a..433dcee23 100644 --- a/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.html +++ b/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.html @@ -459,17 +459,13 @@
+
- -
diff --git a/client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.html b/client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.html index 1282f7e56..6280bd024 100644 --- a/client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.html +++ b/client/src/app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.html @@ -1,13 +1,13 @@
-

{{ poll.title }}

+

{{ motionTitle }}

- - +
+ + +
@@ -45,7 +50,7 @@ close : {{ voteNo }}
-
+
@@ -62,11 +67,7 @@ - diff --git a/client/src/app/site/motions/modules/motion-poll/motion-poll/motion-poll.component.ts b/client/src/app/site/motions/modules/motion-poll/motion-poll/motion-poll.component.ts index 7f65317dd..383c5c2f6 100644 --- a/client/src/app/site/motions/modules/motion-poll/motion-poll/motion-poll.component.ts +++ b/client/src/app/site/motions/modules/motion-poll/motion-poll/motion-poll.component.ts @@ -1,10 +1,12 @@ import { Component, Input } from '@angular/core'; import { MatDialog, MatSnackBar } from '@angular/material'; import { Title } from '@angular/platform-browser'; +import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject } from 'rxjs'; +import { OperatorService } from 'app/core/core-services/operator.service'; import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service'; import { PromptService } from 'app/core/ui-services/prompt.service'; import { ChartData } from 'app/shared/components/charts/charts.component'; @@ -28,7 +30,7 @@ export class MotionPollComponent extends BasePollComponent { */ @Input() public set poll(value: ViewMotionPoll) { - this._poll = value; + this.initPoll(value); const chartData = this.poll.generateChartData(); for (const data of chartData) { @@ -54,17 +56,33 @@ export class MotionPollComponent extends BasePollComponent { /** * Number of votes for `Yes`. */ - public voteYes = 0; + // public voteYes = 0; + public set voteYes(n: number | string) { + this._voteYes = n; + } + + public get voteYes(): number | string { + return this.verboseForNumber(this._voteYes as number); + } /** * Number of votes for `No`. */ - public voteNo = 0; + public set voteNo(n: number | string) { + this._voteNo = n; + } - /** - * The motion-poll. - */ - private _poll: ViewMotionPoll; + public get voteNo(): number | string { + return this.verboseForNumber(this._voteNo as number); + } + + public get showChart(): boolean { + return this._voteYes >= 0 && this._voteNo >= 0; + } + + private _voteNo: number | string = 0; + + private _voteYes: number | string = 0; /** * Constructor. @@ -81,10 +99,30 @@ export class MotionPollComponent extends BasePollComponent { translate: TranslateService, dialog: MatDialog, promptService: PromptService, - public repo: MotionPollRepositoryService, + public pollRepo: MotionPollRepositoryService, pollDialog: MotionPollDialogService, - public pollService: PollService + public pollService: PollService, + private router: Router, + private operator: OperatorService ) { - super(titleService, matSnackBar, translate, dialog, promptService, repo, pollDialog); + super(titleService, matSnackBar, translate, dialog, promptService, pollRepo, pollDialog); + } + + public openPoll(): void { + if (this.operator.hasPerms('motions.can_manage_polls')) { + this.router.navigate(['motions', 'polls', this.poll.id]); + } + } + + private verboseForNumber(input: number): number | string { + input = Math.trunc(input); + switch (input) { + case -1: + return 'Majority'; + case -2: + return 'Not documented'; + default: + return input; + } } } diff --git a/client/src/app/site/motions/services/motion-poll.service.ts b/client/src/app/site/motions/services/motion-poll.service.ts index fb5f19dd9..895dee8df 100644 --- a/client/src/app/site/motions/services/motion-poll.service.ts +++ b/client/src/app/site/motions/services/motion-poll.service.ts @@ -52,7 +52,7 @@ export class MotionPollService extends PollService { const length = this.pollRepo.getViewModelList().filter(item => item.motion_id === poll.motion_id).length; poll.title = !length ? this.translate.instant('Vote') : `${this.translate.instant('Vote')} (${length + 1})`; - poll.pollmethod = MotionPollMethods.YN; + poll.pollmethod = MotionPollMethods.YNA; poll.motion_id = poll.motion_id; } } diff --git a/client/src/app/site/polls/components/base-poll-detail.component.ts b/client/src/app/site/polls/components/base-poll-detail.component.ts index 1972118f5..e71c2d794 100644 --- a/client/src/app/site/polls/components/base-poll-detail.component.ts +++ b/client/src/app/site/polls/components/base-poll-detail.component.ts @@ -11,14 +11,14 @@ import { GroupRepositoryService } from 'app/core/repositories/users/group-reposi import { BasePollDialogService } from 'app/core/ui-services/base-poll-dialog.service'; import { PromptService } from 'app/core/ui-services/prompt.service'; import { Breadcrumb } from 'app/shared/components/breadcrumb/breadcrumb.component'; -import { ChartData } from 'app/shared/components/charts/charts.component'; -import { PollState } from 'app/shared/models/poll/base-poll'; +import { ChartData, ChartType } from 'app/shared/components/charts/charts.component'; +import { PollState, PollType } from 'app/shared/models/poll/base-poll'; import { BaseViewComponent } from 'app/site/base/base-view'; import { ViewGroup } from 'app/site/users/models/view-group'; import { BasePollRepositoryService } from '../services/base-poll-repository.service'; import { ViewBasePoll } from '../models/view-base-poll'; -export class BasePollDetailComponent extends BaseViewComponent implements OnInit { +export abstract class BasePollDetailComponent extends BaseViewComponent implements OnInit { /** * All the groups of users. */ @@ -42,7 +42,8 @@ export class BasePollDetailComponent extends BaseViewCom /** * Sets the type of the shown chart, if votes are entered. */ - public chartType = 'horizontalBar'; + // public chartType = 'horizontalBar'; + public abstract get chartType(): ChartType; /** * The different labels for the votes (used for chart). @@ -99,7 +100,7 @@ export class BasePollDetailComponent extends BaseViewCom const text = 'Do you really want to delete the selected poll?'; if (await this.promptDialog.open(title, text)) { - await this.repo.delete(this.poll); + this.repo.delete(this.poll).then(() => this.onDeleted()); } } @@ -121,12 +122,23 @@ export class BasePollDetailComponent extends BaseViewCom this.chartDataSubject.next(this.poll.generateChartData()); } + protected onDeleted(): void {} + + /** + * Called after the poll has been loaded. Meant to be overwritten by subclasses who need initial access to the poll + */ + protected onPollLoaded(): void {} + + protected onStateChanged(): void {} + + protected abstract hasPerms(): boolean; + /** * This checks, if the poll has votes. */ private checkData(): void { if (this.poll.state === 3 || this.poll.state === 4) { - // this.chartDataSubject.next(this.poll.generateChartData()); + setTimeout(() => this.chartDataSubject.next(this.poll.generateChartData())); } } @@ -142,7 +154,6 @@ export class BasePollDetailComponent extends BaseViewCom this.poll = poll; this.updateBreadcrumbs(); this.checkData(); - this.labels = this.createChartLabels(); this.onPollLoaded(); } }) @@ -157,23 +168,18 @@ export class BasePollDetailComponent extends BaseViewCom this.pollDialog.openDialog(this.poll); } - /** - * Called after the poll has been loaded. Meant to be overwritten by subclasses who need initial access to the poll - */ - public onPollLoaded(): void {} - /** * Action for the different breadcrumbs. */ private async changeState(): Promise { - this.actionWrapper(this.repo.changePollState(this.poll)); + this.actionWrapper(this.repo.changePollState(this.poll), this.onStateChanged); } /** * Resets the state of a motion-poll. */ private async resetState(): Promise { - this.actionWrapper(this.repo.resetPoll(this.poll)); + this.actionWrapper(this.repo.resetPoll(this.poll), this.onStateChanged); } /** @@ -183,17 +189,15 @@ export class BasePollDetailComponent extends BaseViewCom * * @returns Any promise-like. */ - private actionWrapper(action: Promise): any { - action.then(() => this.checkData()).catch(this.raiseError); - } - - /** - * Function to create the labels for the chart. - * - * @returns An array of `Label`. - */ - private createChartLabels(): Label[] { - return ['Number of votes']; + private actionWrapper(action: Promise, callback?: () => any): any { + action + .then(() => { + this.checkData(); + if (callback) { + callback(); + } + }) + .catch(this.raiseError); } /** @@ -220,11 +224,14 @@ export class BasePollDetailComponent extends BaseViewCom if (!this.poll) { return null; } + if (!this.hasPerms()) { + return null; + } switch (this.poll.state) { case PollState.Created: return state === 2 ? () => this.changeState() : null; case PollState.Started: - return null; + return this.poll.type !== PollType.Analog && state === 3 ? () => this.changeState() : null; case PollState.Finished: if (state === 1) { return () => this.resetState(); diff --git a/client/src/app/site/polls/components/base-poll.component.ts b/client/src/app/site/polls/components/base-poll.component.ts index 533fda44a..bb98620a6 100644 --- a/client/src/app/site/polls/components/base-poll.component.ts +++ b/client/src/app/site/polls/components/base-poll.component.ts @@ -1,23 +1,28 @@ -import { Input } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Title } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; +import { BehaviorSubject } from 'rxjs'; import { BasePollDialogService } from 'app/core/ui-services/base-poll-dialog.service'; import { PromptService } from 'app/core/ui-services/prompt.service'; +import { ChartData } from 'app/shared/components/charts/charts.component'; import { PollState } from 'app/shared/models/poll/base-poll'; import { BaseViewComponent } from 'app/site/base/base-view'; import { BasePollRepositoryService } from '../services/base-poll-repository.service'; import { ViewBasePoll } from '../models/view-base-poll'; -export class BasePollComponent extends BaseViewComponent { - /** - * The poll represented in this component - */ - @Input() - public poll: V; +export abstract class BasePollComponent extends BaseViewComponent { + // /** + // * The poll represented in this component + // */ + // @Input() + // public abstract set poll(model: V); + + public chartDataSubject: BehaviorSubject = new BehaviorSubject([]); + + protected _poll: V; public constructor( titleService: Title, @@ -25,14 +30,14 @@ export class BasePollComponent extends BaseViewComponent public translate: TranslateService, public dialog: MatDialog, protected promptService: PromptService, - public repo: BasePollRepositoryService, + protected repo: BasePollRepositoryService, protected pollDialog: BasePollDialogService ) { super(titleService, translate, matSnackBar); } public changeState(key: PollState): void { - key === PollState.Created ? this.repo.resetPoll(this.poll) : this.repo.changePollState(this.poll); + key === PollState.Created ? this.repo.resetPoll(this._poll) : this.repo.changePollState(this._poll); } /** @@ -41,7 +46,7 @@ export class BasePollComponent extends BaseViewComponent public async onDeletePoll(): Promise { const title = this.translate.instant('Are you sure you want to delete this poll?'); if (await this.promptService.open(title)) { - await this.repo.delete(this.poll).catch(this.raiseError); + await this.repo.delete(this._poll).catch(this.raiseError); } } @@ -49,6 +54,13 @@ export class BasePollComponent extends BaseViewComponent * Edits the poll */ public openDialog(): void { - this.pollDialog.openDialog(this.poll); + this.pollDialog.openDialog(this._poll); + } + + /** + * Forces to initialize the poll. + */ + protected initPoll(model: V): void { + this._poll = model; } } diff --git a/client/src/app/site/polls/components/poll-form/poll-form.component.html b/client/src/app/site/polls/components/poll-form/poll-form.component.html index b57d11d21..53667e448 100644 --- a/client/src/app/site/polls/components/poll-form/poll-form.component.html +++ b/client/src/app/site/polls/components/poll-form/poll-form.component.html @@ -47,13 +47,9 @@ - + - + {{ option.value | translate }} @@ -68,4 +64,3 @@
- \ No newline at end of file diff --git a/client/src/app/site/polls/components/poll-form/poll-form.component.ts b/client/src/app/site/polls/components/poll-form/poll-form.component.ts index 5c7cb5de5..d975844da 100644 --- a/client/src/app/site/polls/components/poll-form/poll-form.component.ts +++ b/client/src/app/site/polls/components/poll-form/poll-form.component.ts @@ -63,6 +63,12 @@ export class PollFormComponent extends BaseViewComponent implements OnInit { */ public pollValues: [string, unknown][] = []; + /** + * Model for the checkbox. + * If true, the given poll will immediately be published. + */ + public publishImmediately = true; + /** * Constructor. Retrieves necessary metadata from the pollService, * injects the poll itself @@ -73,18 +79,10 @@ export class PollFormComponent extends BaseViewComponent implements OnInit { snackbar: MatSnackBar, private fb: FormBuilder, private groupRepo: GroupRepositoryService, - private pollService: PollService + public pollService: PollService ) { super(title, translate, snackbar); - - this.contentForm = this.fb.group({ - title: ['', Validators.required], - type: ['', Validators.required], - pollmethod: ['', Validators.required], - onehundred_percent_base: ['', Validators.required], - majority_method: ['', Validators.required], - groups_id: [[]] - }); + this.initContentForm(); } /** @@ -133,6 +131,10 @@ export class PollFormComponent extends BaseViewComponent implements OnInit { return { ...this.data, ...this.contentForm.value }; } + public isValidPercentBaseWithMethod(base: PercentBase): boolean { + return !(base === PercentBase.YNA && this.contentForm.get('pollmethod').value === 'YN'); + } + /** * This updates the poll-values to get correct data in the view. * @@ -152,4 +154,15 @@ export class PollFormComponent extends BaseViewComponent implements OnInit { ]); } } + + private initContentForm(): void { + this.contentForm = this.fb.group({ + title: ['', Validators.required], + type: ['', Validators.required], + pollmethod: ['', Validators.required], + onehundred_percent_base: ['', Validators.required], + majority_method: ['', Validators.required], + groups_id: [[]] + }); + } } diff --git a/client/src/app/site/polls/models/view-base-poll.ts b/client/src/app/site/polls/models/view-base-poll.ts index 6e3623e03..ec10da991 100644 --- a/client/src/app/site/polls/models/view-base-poll.ts +++ b/client/src/app/site/polls/models/view-base-poll.ts @@ -53,6 +53,15 @@ export const PercentBaseVerbose = { }; export abstract class ViewBasePoll = any> extends BaseProjectableViewModel { + private _tableData: {}[] = []; + + public get tableData(): {}[] { + if (!this._tableData.length) { + this._tableData = this.generateTableData(); + } + return this._tableData; + } + public get poll(): M { return this._model; } @@ -101,7 +110,14 @@ export abstract class ViewBasePoll = any> extends Bas public abstract getSlide(): ProjectorElementBuildDeskriptor; + /** + * Initializes labels for a chart. + */ + public abstract initChartLabels(): string[]; + public abstract generateChartData(): ChartData; + + public abstract generateTableData(): {}[]; } export interface ViewBasePoll = any> extends BasePoll { diff --git a/client/src/styles.scss b/client/src/styles.scss index 8cff8d348..d221bde56 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -27,6 +27,7 @@ @import './app/site/config/components/config-field/config-field.component.scss-theme.scss'; @import './app/site/motions/modules/motion-detail/components/amendment-create-wizard/amendment-create-wizard.components.scss-theme.scss'; @import './app/site/motions/modules/motion-detail/components/motion-detail-diff/motion-detail-diff.component.scss-theme.scss'; +@import './app/shared/components/banner/banner.component.scss-theme.scss'; /** fonts */ @import './assets/styles/fonts.scss'; @@ -54,6 +55,7 @@ $narrow-spacing: ( @include os-config-field-style($theme); @include os-amendment-create-wizard-style($theme); @include os-motion-detail-diff-style($theme); + @include os-banner-style($theme); } /** Load projector specific SCSS values */