Polls for motions and assignments

- Adds charts to assignments
- Creates base-classes for polls
This commit is contained in:
GabrielMeyer 2020-01-15 15:12:33 +01:00 committed by FinnStutzenstein
parent 96aa3b0084
commit fff1f15b6c
28 changed files with 570 additions and 386 deletions

View File

@ -1,4 +1,5 @@
<div *ngFor="let banner of banners"
<div
*ngFor="let banner of banners"
class="banner"
[ngClass]="(banner.type === 'history' ? 'history-mode-indicator' : '') + ' ' + (banner.class ? banner.class : '')"
[ngSwitch]="banner.type"

View File

@ -4,12 +4,13 @@
line-height: 20px;
width: 100%;
text-align: center;
background-color: blue;
display: flex;
align-items: center;
justify-content: center;
a {
display: flex;
align-items: center;
text-decoration: none;
color: white;
}

View File

@ -0,0 +1,11 @@
@import '~@angular/material/theming';
/** Custom component theme. Only lives in a specific scope */
@mixin os-banner-style($theme) {
$primary: map-get($theme, primary);
/** style for the offline-banner */
.banner {
background: mat-color($primary, 900);
}
}

View File

@ -1,23 +1,25 @@
<div class="charts-wrapper">
<canvas
*ngIf="type === 'bar' || type === 'line' || type === 'horizontalBar'"
baseChart
[datasets]="chartData"
[labels]="labels"
[legend]="showLegend"
[options]="chartOptions"
[chartType]="type"
(chartClick)="select.emit($event)"
(chartHover)="hover.emit($event)"
>
</canvas>
<canvas
*ngIf="type === 'pie' || type === 'doughnut'"
baseChart
[data]="circleData"
[labels]="circleLabels"
[colors]="circleColors"
[chartType]="type"
[legend]="showLegend"
></canvas>
<ng-container *ngIf="chartData.length || circleData.length">
<canvas
*ngIf="type === 'bar' || type === 'stackedBar' || type === 'horizontalBar' || type === 'line'"
baseChart
[datasets]="chartData"
[labels]="labels"
[legend]="showLegend"
[options]="chartOptions"
[chartType]="type"
(chartClick)="select.emit($event)"
(chartHover)="hover.emit($event)"
>
</canvas>
<canvas
*ngIf="type === 'pie' || type === 'doughnut'"
baseChart
[data]="circleData"
[labels]="circleLabels"
[colors]="circleColors"
[chartType]="type"
[legend]="showLegend"
></canvas>
</ng-container>
</div>

View File

@ -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<ChartData>) {
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;
}
}

View File

@ -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 });
}
})
);

View File

@ -1,7 +1,7 @@
<os-head-bar
[goBack]="true"
[nav]="false"
[hasMainButton]="poll && (poll.state === 2 || poll.state === 3)"
[hasMainButton]="poll ? poll.type === 'analog' && (poll.state === 2 || poll.state === 3) : false"
[mainButtonIcon]="'edit'"
[mainActionTooltip]="'Edit' | translate"
(mainEvent)="openDialog()"
@ -23,7 +23,7 @@
<!-- Detailview for poll -->
<ng-template #viewTemplate>
<ng-container *ngIf="poll">
<ng-container *ngIf="isReady">
<h1>{{ poll.title }}</h1>
<mat-divider></mat-divider>
<os-breadcrumb [breadcrumbs]="breadcrumbs" [breadcrumbStyle]="'>'"></os-breadcrumb>
@ -42,23 +42,62 @@
<div *ngIf="poll.state === 3 || poll.state === 4">
<h2 translate>Result</h2>
<div *ngIf="poll.type === 'named'" style="display: grid; grid-template-columns: auto repeat({{ poll.options.length }}, max-content);">
<div
*ngIf="poll.type === 'named'"
style="display: grid; grid-template-columns: auto repeat({{ poll.options.length }}, max-content);"
>
<!-- top left cell is empty -->
<div></div>
<!-- header (the assignment related users) -->
<ng-container *ngFor="let option of poll.options">
<div *ngIf="option.user">{{ option.user.full_name }}</div>
<div *ngIf="!option.user">{{ "Unknown user" | translate}}</div>
<div *ngIf="!option.user">{{ 'Unknown user' | translate }}</div>
</ng-container>
<!-- rows -->
<ng-container *ngFor="let obj of votesByUser | keyvalue">
<div *ngIf="obj.value.user">{{ obj.value.user.full_name }}</div>
<div *ngIf="!obj.value.user">{{ "Unknown user" | translate}}</div>
<div *ngIf="!obj.value.user">{{ 'Unknown user' | translate }}</div>
<ng-container *ngFor="let option of poll.options">
<div>{{ obj.value.votes[option.user_id]}}</div>
<div>{{ obj.value.votes[option.user_id] }}</div>
</ng-container>
</ng-container>
</div>
<div class="chart-wrapper"></div>
<mat-table [dataSource]="poll.tableData">
<ng-container matColumnDef="user" sticky>
<mat-header-cell *matHeaderCellDef>{{ 'Candidates' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.user }}</mat-cell>
</ng-container>
<ng-container matColumnDef="yes">
<mat-header-cell *matHeaderCellDef>{{ 'Yes' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.yes }}</mat-cell>
</ng-container>
<ng-container matColumnDef="no">
<mat-header-cell *matHeaderCellDef>{{ 'No' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.no }}</mat-cell>
</ng-container>
<ng-container matColumnDef="abstain">
<mat-header-cell *matHeaderCellDef>{{ 'Abstain' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.abstain }}</mat-cell>
</ng-container>
<ng-container matColumnDef="quorum">
<mat-header-cell *matHeaderCellDef>{{ 'Quorum' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row"></mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="columnDefinition"></mat-header-row>
<mat-row *matRowDef="let row; columns: columnDefinition"></mat-row>
</mat-table>
<os-charts
*ngIf="chartDataSubject.value"
[type]="chartType"
[labels]="candidatesLabels"
[showLegend]="true"
[data]="chartDataSubject"
></os-charts>
</div>
</ng-container>
</ng-template>

View File

@ -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<ViewAssignmentPoll> {
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 ((<ViewAssignmentPoll>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<ViewA
route: ActivatedRoute,
groupRepo: GroupRepositoryService,
prompt: PromptService,
pollDialog: AssignmentPollDialogService
pollDialog: AssignmentPollDialogService,
private operator: OperatorService
) {
super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog);
}
@ -54,6 +73,12 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
}
console.log(votes, this.poll, this.poll.options);
this.votesByUser = votes;
}, 1000);
this.candidatesLabels = this.poll.initChartLabels();
this.isReady = true;
});
}
protected hasPerms(): boolean {
return this.operator.hasPerms('assignments.can_manage');
}
}

View File

@ -1,16 +1,18 @@
<ng-container *ngIf="poll">
<ng-container *ngIf="vmanager.canVote(poll)">
<span *ngIf="poll.pollmethod === 'votes'">{{ "You can distribute" | translate }} {{ poll.votes_amount }} {{ "votes" | translate }}.</span>
<span *ngIf="poll.pollmethod === 'votes'"
>{{ 'You can distribute' | translate }} {{ poll.votes_amount }} {{ 'votes' | translate }}.</span
>
<form *ngIf="voteForm" [formGroup]="voteForm" class="voting-grid">
<ng-container *ngFor="let option of poll.options">
<div>
<span *ngIf="option.user">{{ option.user.getFullName() }}</span>
<span *ngIf="!option.user">No user {{ option.candidate_id }}</span>
</div>
<div class="current-vote">
<ng-container *ngIf="currentVotes[option.user_id] !== null">
({{ "Current" | translate }}: {{ getCurrentVoteVerbose(option.user_id) | translate }})
({{ 'Current' | translate }}: {{ getCurrentVoteVerbose(option.user_id) | translate }})
</ng-container>
</div>
@ -31,12 +33,17 @@
</mat-radio-group>
<mat-form-field *ngIf="poll.pollmethod === 'votes'" class="vote-input">
<input matInput type="number" min="0" [formControlName]="option.id">
<input matInput type="number" min="0" [formControlName]="option.id" />
</mat-form-field>
</ng-container>
</form>
<div class="right-align">
<button mat-button mat-button-default (click)="saveVotes()" [disabled]="!voteForm || voteForm.invalid || voteForm.pristine">
<button
mat-button
mat-button-default
(click)="saveVotes()"
[disabled]="!voteForm || voteForm.invalid || voteForm.pristine"
>
<span translate>Save</span>
</button>
</div>
@ -44,4 +51,4 @@
<ng-container *ngIf="!vmanager.canVote(poll)">
<span>{{ vmanager.getVotePermissionErrorVerbose(poll) | translate }}</span>
</ng-container>
</ng-container>
</ng-container>

View File

@ -3,7 +3,7 @@
<!-- Buttons -->
<button
mat-icon-button
*osPerms="'assignments.can_manage'; 'core.can_manage_projector'"
*osPerms="'assignments.can_manage'; &quot;core.can_manage_projector&quot;"
[matMenuTriggerFor]="pollItemMenu"
(click)="$event.stopPropagation()"
>
@ -15,17 +15,6 @@
<mat-icon>edit</mat-icon>
<span translate>Edit</span>
</button>
<!-- <button mat-menu-item (click)="printBallot()">
<mat-icon>local_printshop</mat-icon>
<span translate>Print ballot paper</span>
</button>
<button mat-menu-item (click)="togglePublished()">
<mat-icon>
{{ poll.published ? 'visibility_off' : 'visibility' }}
</mat-icon>
<span *ngIf="!poll.published" translate>Publish</span>
<span *ngIf="poll.published" translate>Unpublish</span>
</button> -->
</div>
<div *osPerms="'core.can_manage_projector'">
<os-projector-button menuItem="true" [object]="poll"></os-projector-button>
@ -40,200 +29,38 @@
</mat-menu>
</div>
<div class="poll-properties">
<!-- <mat-chip *ngIf="pollService.isElectronicVotingEnabled">{{ poll.typeVerbose }}</mat-chip> -->
<mat-chip
class="poll-state active"
[matMenuTriggerFor]="triggerMenu"
[ngClass]="poll.stateVerbose.toLowerCase()"
>
{{ poll.stateVerbose }}
</mat-chip>
<!-- <mat-chip
class="poll-state active"
*ngIf="poll.state !== 2"
[matMenuTriggerFor]="triggerMenu"
[ngClass]="poll.stateVerbose.toLowerCase()"
>
{{ poll.stateVerbose }}
</mat-chip> -->
<!-- <mat-chip class="poll-state" *ngIf="poll.state === 2" [ngClass]="poll.stateVerbose.toLowerCase()">
{{ poll.stateVerbose }}
</mat-chip> -->
</div>
<div>
<div>
<div class="poll-properties">
<!-- <mat-chip *ngIf="pollService.isElectronicVotingEnabled">{{ poll.typeVerbose }}</mat-chip> -->
<mat-chip
class="poll-state active"
[matMenuTriggerFor]="triggerMenu"
[ngClass]="poll.stateVerbose.toLowerCase()"
>
{{ poll.stateVerbose }}
</mat-chip>
</div>
<h3>
<a routerLink="/assignments/polls/{{ poll.id }}">
{{ poll.title }}
</a>
</h3>
<h3>
<a routerLink="/assignments/polls/{{ poll.id }}">
{{ poll.title }}
</a>
</h3>
</div>
<div>
<os-charts [type]="chartType" [labels]="candidatesLabels" [data]="chartDataSubject"></os-charts>
</div>
</div>
<os-assignment-poll-vote *ngIf="poll.canBeVotedFor" [poll]="poll"></os-assignment-poll-vote>
<!-- <ng-container *ngIf="poll.state === pollStates.STATE_PUBLISHED" [ngTemplateOutlet]="resultsTemplate"></ng-container> -->
<div class="poll-main-content" *ngIf="false && poll.options">
<div *ngIf="canSee">
<div class="poll-grid">
<div></div>
<div><span class="table-view-list-title" translate>Candidates</span></div>
<div><span class="table-view-list-title" translate>Votes</span></div>
<div *ngIf="pollService.majorityMethods && majorityChoice && canManage">
<div>
<span class="table-view-list-title" translate>Quorum</span>
</div>
<div>
<!-- manager majority chip (menu trigger) -->
<mat-basic-chip [matMenuTriggerFor]="majorityMenu" class="grey" disableRipple>
{{ majorityChoice.display_name | translate }}
</mat-basic-chip>
<!-- menu for selecting quorum choices -->
<mat-menu #majorityMenu="matMenu">
<button
mat-menu-item
*ngFor="let method of pollService.majorityMethods"
(click)="setMajority(method)"
>
<mat-icon *ngIf="method.value === majorityChoice.value">
check
</mat-icon>
{{ method.display_name | translate }}
</button>
</mat-menu>
</div>
</div>
</div>
<div *ngFor="let option of poll.options" class="poll-grid poll-border">
<div>
<div>
<button
type="button"
mat-icon-button
(click)="toggleElected(option)"
[disabled]="!canManage || poll.assignment.isFinished"
disableRipple
>
<mat-icon
*ngIf="option.is_elected"
class="top-aligned green-text"
matTooltip="{{ 'Elected' | translate }}"
>check_box</mat-icon
>
<mat-icon
*ngIf="!option.is_elected && canManage && !poll.assignment.isFinished"
class="top-aligned primary"
matTooltip="{{ 'Mark as elected' | translate }}"
>
check_box_outline_blank</mat-icon
>
</button>
</div>
</div>
<!-- candidate Name -->
<div class="candidate-name">
{{ option.user.full_name }}
</div>
<!-- Votes -->
<div>
<div *ngIf="poll.has_votes">
<div *ngFor="let vote of option.votes" class="spacer-bottom-10">
<div class="poll-progress">
<span *ngIf="vote.value !== 'Votes'"
>{{ pollService.getLabel(vote.value) | translate }}:</span
>
{{ pollService.getSpecialLabel(vote.weight) | translate }}
<span *ngIf="!pollService.isAbstractOption(poll, option, vote.value)"
>({{ pollService.getPercent(poll, option, vote.value) }}%)</span
>
</div>
<div
*ngIf="!pollService.isAbstractOption(poll, option, vote.value)"
class="poll-progress-bar"
>
<mat-progress-bar
mode="determinate"
[value]="pollService.getPercent(poll, option, vote.value)"
[ngClass]="pollService.getProgressBarColor(vote.value)"
>
</mat-progress-bar>
</div>
</div>
</div>
</div>
<div *ngIf="canManage">
<div
*ngIf="
poll.has_votes &&
majorityChoice &&
majorityChoice.value !== 'disabled' &&
!pollService.isAbstractOption(poll, option)
"
class="poll-quorum"
>
<span>{{ pollService.yesQuorum(majorityChoice, poll, option) }}</span>
<span
[ngClass]="quorumReached(option) ? 'green-text' : 'red-warning-text'"
matTooltip="{{ getQuorumReachedString(option) }}"
>
<mat-icon *ngIf="quorumReached(option)">{{ pollService.getIcon('yes') }}</mat-icon>
<mat-icon *ngIf="!quorumReached(option)">{{ pollService.getIcon('no') }}</mat-icon>
</span>
</div>
</div>
</div>
<!-- summary -->
<div>
<div *ngFor="let key of pollValues" class="poll-grid">
<div></div>
<div class="candidate-name">
<span>{{ pollService.getLabel(key) | translate }}</span
>:
</div>
<div *ngIf="poll[key]">
{{ pollService.getSpecialLabel(poll[key]) | translate }}
<span *ngIf="!pollService.isAbstractValue(poll, key)">
({{ pollService.getValuePercent(poll, key) }} %)
</span>
</div>
</div>
</div>
</div>
<div *ngIf="!pollData">
<h4 translate>Candidates</h4>
<div *ngFor="let option of poll.options">
<span class="accent" *ngIf="option.user">{{ option.user.getFullName() }}</span>
<span *ngIf="!option.user">No user {{ option.candidate_id }}</span>
</div>
</div>
</div>
<!-- Election Method -->
<!-- <div *ngIf="canManage" class="spacer-bottom-10">
<h4 translate>Election method</h4>
<span>{{ pollMethodName | translate }}</span>
</div> -->
<!-- Poll paper hint -->
<!-- <div *ngIf="canManage" class="hint-form" [formGroup]="descriptionForm">
<mat-form-field class="wide">
<mat-label translate>Hint for ballot paper</mat-label>
<input matInput formControlName="description" />
</mat-form-field>
<button mat-icon-button [disabled]="!dirtyDescription" (click)="onEditDescriptionButton()">
<mat-icon inline>check</mat-icon>
</button>
</div> -->
</div>
<ng-template #resultsTemplate>
</ng-template>
<ng-template #resultsTemplate> </ng-template>
<mat-menu #triggerMenu="matMenu">
<ng-container *ngIf="poll">
<button
mat-menu-item
(click)="changeState(state.value)"
*ngFor="let state of poll.nextStates | keyvalue"
>
<button mat-menu-item (click)="changeState(state.value)" *ngFor="let state of poll.nextStates | keyvalue">
<span translate>{{ state.key }}</span>
</button>
</ng-container>

View File

@ -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<ViewAssignmentPoll> 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<ViewAssignmentPol
promptService: PromptService,
repo: AssignmentPollRepositoryService,
pollDialog: AssignmentPollDialogService,
public pollService: PollService,
private operator: OperatorService,
private formBuilder: FormBuilder
) {

View File

@ -1,5 +1,8 @@
import { BehaviorSubject } from 'rxjs';
import { ChartData } from 'app/shared/components/charts/charts.component';
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
import { AssignmentPoll, AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
import { PollColor } from 'app/shared/models/poll/base-poll';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { ViewBasePoll } from 'app/site/polls/models/view-base-poll';
import { ViewAssignment } from './view-assignment';
@ -19,8 +22,13 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll> implements
public static COLLECTIONSTRING = AssignmentPoll.COLLECTIONSTRING;
protected _collectionString = AssignmentPoll.COLLECTIONSTRING;
public readonly tableChartData: Map<string, BehaviorSubject<ChartData>> = 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<AssignmentPoll> 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;
}
}

View File

@ -20,6 +20,20 @@ export class ViewMotionPoll extends ViewBasePoll<MotionPoll> 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<MotionPoll> 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;
}

View File

@ -459,17 +459,13 @@
<!-- motion polls -->
<div *ngIf="!editMotion" class="spacer-top-20 spacer-bottom-20">
<os-motion-poll *ngFor="let poll of motion.polls; trackBy: trackByIndex" [poll]="poll"></os-motion-poll>
<div class="mat-card create-poll-button" *ngIf="perms.isAllowed('createpoll', motion)">
<button mat-button (click)="openDialog()">
<mat-icon class="main-nav-color">poll</mat-icon>
<span translate>New poll</span>
</button>
</div>
<os-motion-poll
*ngFor="let poll of motion.polls; trackBy: trackByIndex"
[poll]="poll"
></os-motion-poll>
<!-- <os-motion-poll-manager [motion]="motion"></os-motion-poll-manager> -->
</div>
</div>
</ng-template>

View File

@ -1,13 +1,13 @@
<os-head-bar
[goBack]="true"
[nav]="false"
[hasMainButton]="poll && (poll.state === 2 || poll.state === 3)"
[hasMainButton]="poll ? poll.state === 2 || poll.state === 3 : false"
[mainButtonIcon]="'edit'"
[mainActionTooltip]="'Edit' | translate"
(mainEvent)="openDialog()"
>
<div class="title-slot">
<h2 *ngIf="!!poll">{{ poll.title }}</h2>
<h2 *ngIf="!!poll">{{ motionTitle }}</h2>
</div>
<div class="menu-slot" *osPerms="'agenda.can_manage'; or: 'agenda.can_see_list_of_speakers'">
@ -27,38 +27,55 @@
<h1>{{ poll.title }}</h1>
<mat-divider></mat-divider>
<os-breadcrumb [breadcrumbs]="breadcrumbs" [breadcrumbStyle]="'>'"></os-breadcrumb>
<div *ngIf="poll.state === 3 || poll.state === 4">
<h2 translate>Result</h2>
<os-charts
*ngIf="chartDataSubject.value"
[type]="chartType"
[showLegend]="true"
[data]="chartDataSubject"
></os-charts>
<div
*ngIf="poll.type === 'named'"
style="display: grid; grid-template-columns: max-content auto;grid-column-gap: 20px;"
>
<ng-container *ngFor="let vote of poll.options[0].votes">
<div *ngIf="vote.user">{{ vote.user.full_name }}</div>
<div *ngIf="!vote.user">{{ 'Unknown user' | translate }}</div>
<div>{{ vote.valueVerbose }}</div>
</ng-container>
</div>
<mat-table [dataSource]="poll.tableData">
<ng-container matColumnDef="key" sticky>
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.key }}</mat-cell>
</ng-container>
<ng-container matColumnDef="value" sticky>
<mat-header-cell *matHeaderCellDef>Votes</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.value }}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="columnDefinition"></mat-header-row>
<mat-row *matRowDef="let row; columns: columnDefinition"></mat-row>
</mat-table>
</div>
<div class="poll-content">
<div>{{ 'Current state' | translate }}: {{ poll.stateVerbose | translate }}</div>
<div *ngIf="poll.groups && poll.type && poll.type !== 'analog'">
{{ 'Groups' | translate }}:
<span *ngFor="let group of poll.groups">{{ group.getTitle() | translate }}</span>
<span *ngFor="let group of poll.groups; let i = index">
{{ group.getTitle() | translate }}
<span *ngIf="i < poll.groups.length - 1">, </span>
</span>
</div>
<div>{{ 'Poll type' | translate }}: {{ poll.typeVerbose | translate }}</div>
<div>{{ 'Poll method' | translate }}: {{ poll.pollmethodVerbose | translate }}</div>
<div>{{ 'Majority method' | translate }}: {{ poll.majorityMethodVerbose | translate }}</div>
<div>{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}</div>
</div>
<div *ngIf="poll.state === 3 || poll.state === 4">
<h2 translate>Result</h2>
<!-- <div class="chart-wrapper">
<mat-form-field>
<mat-select [(ngModel)]="chartType" [placeholder]="'Display as' | translate">
<mat-option [value]="'horizontalBar'">{{ 'Bar' | translate }}</mat-option>
<mat-option [value]="'doughnut'">{{ 'Circle' | translate }}</mat-option>
</mat-select>
</mat-form-field>
</div>
<os-charts [type]="chartType" [labels]="labels" [showLegend]="true" [data]="chartDataSubject"></os-charts> -->
<div *ngIf="poll.type === 'named'" style="display: grid; grid-template-columns: max-content auto;grid-column-gap: 20px;">
<ng-container *ngFor="let vote of poll.options[0].votes">
<div *ngIf="vote.user">{{ vote.user.full_name }}</div>
<div *ngIf="!vote.user">{{ "Unknown user" | translate}}</div>
<div>{{ vote.valueVerbose }}</div>
</ng-container>
</div>
</div>
</ng-container>
</ng-template>

View File

@ -2,8 +2,13 @@
padding-top: 10px;
}
.result-title {
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.chart-wrapper {
display: flex;
padding: 16px;
text-align: center;
justify-content: space-around;
align-items: center;
* {

View File

@ -1,23 +1,40 @@
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { MatSnackBar } from '@angular/material';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { OperatorService } from 'app/core/core-services/operator.service';
import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-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 { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
import { MotionPollDialogService } from 'app/site/motions/services/motion-poll-dialog.service';
import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-detail.component';
// import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
@Component({
selector: 'os-motion-poll-detail',
templateUrl: './motion-poll-detail.component.html',
styleUrls: ['./motion-poll-detail.component.scss']
})
export class MotionPollDetailComponent extends BasePollDetailComponent<ViewMotionPoll> {
export class MotionPollDetailComponent extends BasePollDetailComponent<ViewMotionPoll> implements OnInit {
public motionTitle = '';
public columnDefinition = ['key', 'value'];
public set chartType(type: ChartType) {
this._chartType = type;
}
public get chartType(): ChartType {
return this._chartType;
}
private _chartType: ChartType = 'doughnut';
public constructor(
title: Title,
translate: TranslateService,
@ -26,8 +43,27 @@ export class MotionPollDetailComponent extends BasePollDetailComponent<ViewMotio
route: ActivatedRoute,
groupRepo: GroupRepositoryService,
prompt: PromptService,
pollDialog: MotionPollDialogService
pollDialog: MotionPollDialogService,
private operator: OperatorService,
private router: Router,
private motionRepo: MotionRepositoryService
) {
super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog);
}
protected onPollLoaded(): void {
this.motionTitle = this.motionRepo.getViewModel((<ViewMotionPoll>this.poll).motion_id).getTitle();
}
public openDialog(): void {
this.pollDialog.openDialog(this.poll);
}
protected onDeleted(): void {
this.router.navigate(['motions', (<ViewMotionPoll>this.poll).motion_id]);
}
protected hasPerms(): boolean {
return this.operator.hasPerms('motions.can_manage_polls');
}
}

View File

@ -18,7 +18,6 @@
formControlName="N"
></os-check-input>
<os-check-input
*ngIf="pollForm.contentForm.get('pollmethod').value === motionPollMethods.YNA"
[placeholder]="'Abstain' | translate"
[checkboxValue]="-1"
inputType="number"
@ -46,10 +45,7 @@
</div>
<mat-divider></mat-divider>
<div class="spacer-top-20">
<mat-checkbox
[(ngModel)]="publishImmediately"
(change)="publishStateChanged($event.checked)"
>
<mat-checkbox [(ngModel)]="publishImmediately" (change)="publishStateChanged($event.checked)">
<span translate>Publish immediately</span>
</mat-checkbox>
<mat-error *ngIf="!dialogVoteForm.valid" translate>

View File

@ -5,7 +5,6 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { MotionPollMethods } from 'app/shared/models/motions/motion-poll';
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
import { MotionPollMethodsVerbose } from 'app/site/motions/models/view-motion-poll';
import { BasePollDialogComponent } from 'app/site/polls/components/base-poll-dialog.component';
@ -17,7 +16,7 @@ import { PollFormComponent } from 'app/site/polls/components/poll-form/poll-form
styleUrls: ['./motion-poll-dialog.component.scss']
})
export class MotionPollDialogComponent extends BasePollDialogComponent {
public motionPollMethods = MotionPollMethodsVerbose;
public motionPollMethods = { YNA: MotionPollMethodsVerbose.YNA };
@ViewChild('pollForm', { static: false })
protected pollForm: PollFormComponent;
@ -38,13 +37,14 @@ export class MotionPollDialogComponent extends BasePollDialogComponent {
const update: any = {
Y: data.options[0].yes,
N: data.options[0].no,
A: data.options[0].abstain,
votesvalid: data.votesvalid,
votesinvalid: data.votesinvalid,
votescast: data.votescast
};
if (data.pollmethod === 'YNA') {
update.A = data.options[0].abstain;
}
// if (data.pollmethod === 'YNA') {
// update.A = data.options[0].abstain;
// }
if (this.dialogVoteForm) {
const result = this.undoReplaceEmptyValues(update);
@ -59,13 +59,14 @@ export class MotionPollDialogComponent extends BasePollDialogComponent {
this.dialogVoteForm = this.fb.group({
Y: ['', [Validators.min(-2)]],
N: ['', [Validators.min(-2)]],
A: ['', [Validators.min(-2)]],
votesvalid: ['', [Validators.min(-2)]],
votesinvalid: ['', [Validators.min(-2)]],
votescast: ['', [Validators.min(-2)]]
});
if (this.pollData.pollmethod === MotionPollMethods.YNA) {
this.dialogVoteForm.addControl('A', this.fb.control('', [Validators.min(-2)]));
}
// if (this.pollData.pollmethod === MotionPollMethods.YNA) {
// this.dialogVoteForm.addControl('A', this.fb.control('', [Validators.min(-2)]));
// }
if (this.pollData.poll) {
this.updateDialogVoteForm(this.pollData);
}

View File

@ -31,8 +31,13 @@
</div>
</div>
<div class="poll-chart-wrapper" *ngIf="poll">
<ng-container *ngIf="poll.state === 3 || poll.state === 4" [ngTemplateOutlet]="viewTemplate"></ng-container>
<ng-container *ngIf="(poll.state === 1 || poll.state === 2) && poll.type === 'analog'" [ngTemplateOutlet]="emptyTemplate"></ng-container>
<div *ngIf="poll.type === 'analog' || poll.state === 3 || poll.state === 4" (click)="openPoll()">
<ng-container *ngIf="poll.state === 3 || poll.state === 4" [ngTemplateOutlet]="viewTemplate"></ng-container>
<ng-container
*ngIf="(poll.state === 1 || poll.state === 2) && poll.type === 'analog'"
[ngTemplateOutlet]="emptyTemplate"
></ng-container>
</div>
<ng-container *ngIf="(poll.state === 1 || poll.state === 2) && poll.type !== 'analog'">
<os-motion-poll-vote [poll]="poll"></os-motion-poll-vote>
</ng-container>
@ -45,7 +50,7 @@
<mat-icon>close</mat-icon>
: {{ voteNo }}
</div>
<div class="doughnut-chart">
<div *ngIf="showChart" class="doughnut-chart">
<os-charts [type]="'doughnut'" [data]="chartDataSubject" [showLegend]="false"> </os-charts>
</div>
<div class="chart-wrapper-right">
@ -62,11 +67,7 @@
<mat-menu #triggerMenu="matMenu">
<ng-container *ngIf="poll">
<button
mat-menu-item
(click)="changeState(state.value)"
*ngFor="let state of poll.nextStates | keyvalue"
>
<button mat-menu-item (click)="changeState(state.value)" *ngFor="let state of poll.nextStates | keyvalue">
<span translate>{{ state.key }}</span>
</button>
</ng-container>

View File

@ -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<ViewMotionPoll> {
*/
@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<ViewMotionPoll> {
/**
* 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<ViewMotionPoll> {
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;
}
}
}

View File

@ -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;
}
}

View File

@ -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<V extends ViewBasePoll> extends BaseViewComponent implements OnInit {
export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends BaseViewComponent implements OnInit {
/**
* All the groups of users.
*/
@ -42,7 +42,8 @@ export class BasePollDetailComponent<V extends ViewBasePoll> 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<V extends ViewBasePoll> 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<V extends ViewBasePoll> 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<V extends ViewBasePoll> extends BaseViewCom
this.poll = poll;
this.updateBreadcrumbs();
this.checkData();
this.labels = this.createChartLabels();
this.onPollLoaded();
}
})
@ -157,23 +168,18 @@ export class BasePollDetailComponent<V extends ViewBasePoll> 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<void> {
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<void> {
this.actionWrapper(this.repo.resetPoll(this.poll));
this.actionWrapper(this.repo.resetPoll(this.poll), this.onStateChanged);
}
/**
@ -183,17 +189,15 @@ export class BasePollDetailComponent<V extends ViewBasePoll> extends BaseViewCom
*
* @returns Any promise-like.
*/
private actionWrapper(action: Promise<any>): 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<any>, callback?: () => any): any {
action
.then(() => {
this.checkData();
if (callback) {
callback();
}
})
.catch(this.raiseError);
}
/**
@ -220,11 +224,14 @@ export class BasePollDetailComponent<V extends ViewBasePoll> 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();

View File

@ -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<V extends ViewBasePoll> extends BaseViewComponent {
/**
* The poll represented in this component
*/
@Input()
public poll: V;
export abstract class BasePollComponent<V extends ViewBasePoll> extends BaseViewComponent {
// /**
// * The poll represented in this component
// */
// @Input()
// public abstract set poll(model: V);
public chartDataSubject: BehaviorSubject<ChartData> = new BehaviorSubject([]);
protected _poll: V;
public constructor(
titleService: Title,
@ -25,14 +30,14 @@ export class BasePollComponent<V extends ViewBasePoll> extends BaseViewComponent
public translate: TranslateService,
public dialog: MatDialog,
protected promptService: PromptService,
public repo: BasePollRepositoryService,
protected repo: BasePollRepositoryService,
protected pollDialog: BasePollDialogService<V>
) {
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<V extends ViewBasePoll> extends BaseViewComponent
public async onDeletePoll(): Promise<void> {
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<V extends ViewBasePoll> 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;
}
}

View File

@ -47,13 +47,9 @@
</ng-container>
<mat-form-field>
<mat-select
placeholder="{{ '100% base' | translate }}"
formControlName="onehundred_percent_base"
required
>
<mat-select placeholder="{{ '100% base' | translate }}" formControlName="onehundred_percent_base" required>
<ng-container *ngFor="let option of percentBases | keyvalue">
<mat-option [value]="option.key">
<mat-option *ngIf="isValidPercentBaseWithMethod(option.key)" [value]="option.key">
{{ option.value | translate }}
</mat-option>
</ng-container>
@ -68,4 +64,3 @@
</mat-form-field>
</form>
</div>

View File

@ -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: [[]]
});
}
}

View File

@ -53,6 +53,15 @@ export const PercentBaseVerbose = {
};
export abstract class ViewBasePoll<M extends BasePoll<M, any> = any> extends BaseProjectableViewModel<M> {
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<M extends BasePoll<M, any> = 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<M extends BasePoll<M, any> = any> extends BasePoll<M, any> {

View File

@ -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 */