Enhance voting ux
This commit is contained in:
parent
7ab5346198
commit
604df9d48b
@ -13,6 +13,9 @@ export enum VotingError {
|
|||||||
USER_HAS_VOTED
|
USER_HAS_VOTED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: It appears that the only message that makes sense for the user to see it the last one.
|
||||||
|
*/
|
||||||
export const VotingErrorVerbose = {
|
export const VotingErrorVerbose = {
|
||||||
1: "You can't vote on this poll right now because it's not in the 'Started' state.",
|
1: "You can't vote on this poll right now because it's not in the 'Started' state.",
|
||||||
2: "You can't vote on this poll because its type is set to analog voting.",
|
2: "You can't vote on this poll because its type is set to analog voting.",
|
||||||
|
@ -1,17 +1,14 @@
|
|||||||
<ol class="breadcrumb-list">
|
<div>
|
||||||
<li *ngFor="let breadcrumb of breadcrumbList" class="breadcrumb" [ngClass]="{ active: breadcrumb.active }">
|
<mat-button-toggle-group>
|
||||||
<ng-container *ngIf="breadcrumb.active">
|
<mat-button-toggle
|
||||||
|
*ngFor="let breadcrumb of breadcrumbList"
|
||||||
|
[disabled]="breadcrumb.action === null"
|
||||||
|
(click)="breadcrumb.action ? breadcrumb.action() : null"
|
||||||
|
[ngClass]="{ 'active-breadcrumb': breadcrumb.active }"
|
||||||
|
>
|
||||||
<span>
|
<span>
|
||||||
{{ breadcrumb.label }}
|
{{ breadcrumb.label }}
|
||||||
</span>
|
</span>
|
||||||
</ng-container>
|
</mat-button-toggle>
|
||||||
<ng-container *ngIf="!breadcrumb.active">
|
</mat-button-toggle-group>
|
||||||
<span
|
</div>
|
||||||
(click)="breadcrumb.action ? breadcrumb.action() : null"
|
|
||||||
[ngClass]="{ 'accent-foreground has-action': breadcrumb.action }"
|
|
||||||
>
|
|
||||||
{{ breadcrumb.label }}
|
|
||||||
</span>
|
|
||||||
</ng-container>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
|
@ -1,25 +1,4 @@
|
|||||||
$breadcrumb-content: var(--breadcrumb-content);
|
.active-breadcrumb {
|
||||||
|
// Theme
|
||||||
.breadcrumb-list {
|
color: rgba($color: #317796, $alpha: 1);
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb {
|
|
||||||
& + & {
|
|
||||||
padding-left: 8px;
|
|
||||||
&::before {
|
|
||||||
padding-right: 8px;
|
|
||||||
content: $breadcrumb-content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span.has-action {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,8 @@ export class BreadcrumbComponent implements OnInit {
|
|||||||
@Input()
|
@Input()
|
||||||
public set breadcrumbs(labels: string[] | Breadcrumb[]) {
|
public set breadcrumbs(labels: string[] | Breadcrumb[]) {
|
||||||
this.breadcrumbList = [];
|
this.breadcrumbList = [];
|
||||||
|
|
||||||
|
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-3.html#caveats
|
||||||
for (const breadcrumb of labels) {
|
for (const breadcrumb of labels) {
|
||||||
if (typeof breadcrumb === 'string') {
|
if (typeof breadcrumb === 'string') {
|
||||||
this.breadcrumbList.push({ label: breadcrumb, action: null });
|
this.breadcrumbList.push({ label: breadcrumb, action: null });
|
||||||
@ -45,16 +47,6 @@ export class BreadcrumbComponent implements OnInit {
|
|||||||
this.breadcrumbList[index].active = true;
|
this.breadcrumbList[index].active = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the separator for the breadcrumbs.
|
|
||||||
*
|
|
||||||
* @param style The new separator as string (character).
|
|
||||||
*/
|
|
||||||
@Input()
|
|
||||||
public set breadcrumbStyle(style: string) {
|
|
||||||
document.documentElement.style.setProperty('--breadcrumb-content', `'${style}'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list of the breadcrumbs built by the input.
|
* The list of the breadcrumbs built by the input.
|
||||||
*/
|
*/
|
||||||
@ -63,9 +55,7 @@ export class BreadcrumbComponent implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* Default constructor.
|
* Default constructor.
|
||||||
*/
|
*/
|
||||||
public constructor() {
|
public constructor() {}
|
||||||
this.breadcrumbStyle = '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OnInit.
|
* OnInit.
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
<canvas
|
<canvas
|
||||||
*ngIf="type === 'pie' || type === 'doughnut'"
|
*ngIf="type === 'pie' || type === 'doughnut'"
|
||||||
baseChart
|
baseChart
|
||||||
|
[options]="pieChartOptions"
|
||||||
[data]="circleData"
|
[data]="circleData"
|
||||||
[labels]="circleLabels"
|
[labels]="circleLabels"
|
||||||
[colors]="circleColors"
|
[colors]="circleColors"
|
||||||
|
@ -177,6 +177,13 @@ export class ChartsComponent extends BaseViewComponent {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chart option for pie and doughnut
|
||||||
|
*/
|
||||||
|
public pieChartOptions: ChartOptions = {
|
||||||
|
aspectRatio: 1
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds the type of the chart - defaults to `bar`.
|
* Holds the type of the chart - defaults to `bar`.
|
||||||
*/
|
*/
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
[name]="'checkbox'"
|
[name]="'checkbox'"
|
||||||
[ngModel]="isChecked"
|
[ngModel]="isChecked"
|
||||||
(change)="checkboxStateChanged($event.checked)"
|
(change)="checkboxStateChanged($event.checked)"
|
||||||
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
{{ checkboxLabel }}
|
{{ checkboxLabel }}
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
|
@ -52,6 +52,13 @@ export abstract class BasePoll<T = any, O extends BaseOption<any> = any> extends
|
|||||||
public onehundred_percent_base: PercentBase;
|
public onehundred_percent_base: PercentBase;
|
||||||
public user_has_voted: boolean;
|
public user_has_voted: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the state is finished or published
|
||||||
|
*/
|
||||||
|
public get stateHasVotes(): boolean {
|
||||||
|
return this.state === PollState.Finished || this.state === PollState.Published;
|
||||||
|
}
|
||||||
|
|
||||||
protected getDecimalFields(): (keyof BasePoll<T, O>)[] {
|
protected getDecimalFields(): (keyof BasePoll<T, O>)[] {
|
||||||
return ['votesvalid', 'votesinvalid', 'votescast'];
|
return ['votesvalid', 'votesinvalid', 'votescast'];
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
<ng-container *ngIf="isReady">
|
<ng-container *ngIf="isReady">
|
||||||
<h1>{{ poll.title }}</h1>
|
<h1>{{ poll.title }}</h1>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<os-breadcrumb [breadcrumbs]="breadcrumbs" [breadcrumbStyle]="'>'"></os-breadcrumb>
|
<os-breadcrumb [breadcrumbs]="breadcrumbs"></os-breadcrumb>
|
||||||
<div class="poll-content">
|
<div class="poll-content">
|
||||||
<div>{{ 'Current state' | translate }}: {{ poll.stateVerbose | translate }}</div>
|
<div>{{ 'Current state' | translate }}: {{ poll.stateVerbose | translate }}</div>
|
||||||
<div *ngIf="poll.groups && poll.type && poll.type !== 'analog'">
|
<div *ngIf="poll.groups && poll.type && poll.type !== 'analog'">
|
||||||
|
@ -23,6 +23,10 @@ export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPo
|
|||||||
private tableKeys = ['yes', 'no', 'abstain'];
|
private tableKeys = ['yes', 'no', 'abstain'];
|
||||||
private voteKeys = ['votesvalid', 'votesinvalid', 'votescast'];
|
private voteKeys = ['votesvalid', 'votesinvalid', 'votescast'];
|
||||||
|
|
||||||
|
public get hasVotes(): boolean {
|
||||||
|
return !!this.options[0].votes.length;
|
||||||
|
}
|
||||||
|
|
||||||
public initChartLabels(): string[] {
|
public initChartLabels(): string[] {
|
||||||
return ['Votes'];
|
return ['Votes'];
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,6 @@
|
|||||||
<os-head-bar
|
<os-head-bar [goBack]="true" [nav]="false">
|
||||||
[goBack]="true"
|
|
||||||
[nav]="false"
|
|
||||||
[hasMainButton]="poll ? poll.state === 2 || poll.state === 3 : false"
|
|
||||||
[mainButtonIcon]="'edit'"
|
|
||||||
[mainActionTooltip]="'Edit' | translate"
|
|
||||||
(mainEvent)="openDialog()"
|
|
||||||
>
|
|
||||||
<div class="title-slot">
|
<div class="title-slot">
|
||||||
<h2 *ngIf="!!poll">{{ motionTitle }}</h2>
|
<h2 *ngIf="motion">{{ 'Motion' | translate }} {{ motion.id }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="menu-slot" *osPerms="'agenda.can_manage'; or: 'agenda.can_see_list_of_speakers'">
|
<div class="menu-slot" *osPerms="'agenda.can_manage'; or: 'agenda.can_see_list_of_speakers'">
|
||||||
@ -25,29 +18,26 @@
|
|||||||
<ng-template #viewTemplate>
|
<ng-template #viewTemplate>
|
||||||
<ng-container *ngIf="poll">
|
<ng-container *ngIf="poll">
|
||||||
<h1>{{ poll.title }}</h1>
|
<h1>{{ poll.title }}</h1>
|
||||||
<mat-divider></mat-divider>
|
<span *ngIf="poll.type !== 'analog'">{{ 'Polly type' | translate }}: {{ poll.type | translate }}</span>
|
||||||
<os-breadcrumb [breadcrumbs]="breadcrumbs" [breadcrumbStyle]="'>'"></os-breadcrumb>
|
<os-breadcrumb [breadcrumbs]="breadcrumbs"></os-breadcrumb>
|
||||||
|
|
||||||
<div *ngIf="poll.state === 3 || poll.state === 4">
|
<div *ngIf="poll.stateHasVotes">
|
||||||
<h2 translate>Result</h2>
|
<h2 translate>Result</h2>
|
||||||
|
|
||||||
|
<div *ngIf="!poll.hasVotes">{{ 'No results to show' | translate }}</div>
|
||||||
|
|
||||||
|
<div class="result-wrapper" *ngIf="poll.hasVotes">
|
||||||
|
<!-- Chart -->
|
||||||
<os-charts
|
<os-charts
|
||||||
|
class="result-chart"
|
||||||
*ngIf="chartDataSubject.value"
|
*ngIf="chartDataSubject.value"
|
||||||
[type]="chartType"
|
[type]="chartType"
|
||||||
[showLegend]="true"
|
[showLegend]="true"
|
||||||
[data]="chartDataSubject"
|
[data]="chartDataSubject"
|
||||||
></os-charts>
|
></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">
|
<!-- result table -->
|
||||||
|
<mat-table class="result-table" [dataSource]="poll.tableData">
|
||||||
<ng-container matColumnDef="key" sticky>
|
<ng-container matColumnDef="key" sticky>
|
||||||
<mat-header-cell *matHeaderCellDef></mat-header-cell>
|
<mat-header-cell *matHeaderCellDef></mat-header-cell>
|
||||||
<mat-cell *matCellDef="let row">{{ row.key }}</mat-cell>
|
<mat-cell *matCellDef="let row">{{ row.key }}</mat-cell>
|
||||||
@ -60,19 +50,29 @@
|
|||||||
<mat-header-row *matHeaderRowDef="columnDefinition"></mat-header-row>
|
<mat-header-row *matHeaderRowDef="columnDefinition"></mat-header-row>
|
||||||
<mat-row *matRowDef="let row; columns: columnDefinition"></mat-row>
|
<mat-row *matRowDef="let row; columns: columnDefinition"></mat-row>
|
||||||
</mat-table>
|
</mat-table>
|
||||||
|
|
||||||
|
<!-- Named table -->
|
||||||
|
<!-- The table was created in another PR -->
|
||||||
|
<div class="named-result-table" *ngIf="poll.type === 'named'">
|
||||||
|
<h3>{{ 'Singe votes' | translate }}</h3>
|
||||||
|
|
||||||
|
<div *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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="poll-content">
|
<div class="poll-content small">
|
||||||
<div>{{ 'Current state' | translate }}: {{ poll.stateVerbose | translate }}</div>
|
|
||||||
<div *ngIf="poll.groups && poll.type && poll.type !== 'analog'">
|
<div *ngIf="poll.groups && poll.type && poll.type !== 'analog'">
|
||||||
{{ 'Groups' | translate }}:
|
{{ 'Groups' | translate }}:
|
||||||
|
|
||||||
<span *ngFor="let group of poll.groups; let i = index">
|
<span *ngFor="let group of poll.groups; let i = index">
|
||||||
{{ group.getTitle() | translate }}
|
{{ group.getTitle() | translate }}<span *ngIf="i < poll.groups.length - 1">, </span>
|
||||||
<span *ngIf="i < poll.groups.length - 1">, </span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>{{ 'Majority method' | translate }}: {{ poll.majorityMethodVerbose | translate }}</div>
|
||||||
<div>{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}</div>
|
<div>{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -82,13 +82,17 @@
|
|||||||
<!-- More Menu -->
|
<!-- More Menu -->
|
||||||
<mat-menu #pollDetailMenu="matMenu">
|
<mat-menu #pollDetailMenu="matMenu">
|
||||||
<os-projector-button [menuItem]="true" [object]="poll" *osPerms="'core.can_manage_projector'"></os-projector-button>
|
<os-projector-button [menuItem]="true" [object]="poll" *osPerms="'core.can_manage_projector'"></os-projector-button>
|
||||||
|
<button mat-menu-item (click)="openDialog()">
|
||||||
|
<mat-icon>edit</mat-icon>
|
||||||
|
<span translate>Edit</span>
|
||||||
|
</button>
|
||||||
<button mat-menu-item *ngIf="poll && poll.type === 'named'" (click)="pseudoanonymizePoll()">
|
<button mat-menu-item *ngIf="poll && poll.type === 'named'" (click)="pseudoanonymizePoll()">
|
||||||
<mat-icon>questionmark</mat-icon>
|
<mat-icon>questionmark</mat-icon>
|
||||||
<span translate>Pseudoanonymize</span>
|
<span translate>Pseudoanonymize</span>
|
||||||
</button>
|
</button>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<button mat-menu-item (click)="deletePoll()">
|
<button mat-menu-item (click)="deletePoll()">
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon color="warn">delete</mat-icon>
|
||||||
<span translate>Delete</span>
|
<span translate>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
@ -1,18 +1,38 @@
|
|||||||
|
@import '~assets/styles/variables.scss';
|
||||||
|
|
||||||
.poll-content {
|
.poll-content {
|
||||||
padding-top: 10px;
|
padding-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-title {
|
.result-wrapper {
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
display: grid;
|
||||||
|
grid-gap: 10px;
|
||||||
|
grid-template-areas:
|
||||||
|
'chart'
|
||||||
|
'results'
|
||||||
|
'names';
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-wrapper {
|
@include desktop {
|
||||||
padding: 16px;
|
.result-wrapper {
|
||||||
text-align: center;
|
grid-template-areas:
|
||||||
justify-content: space-around;
|
'results chart'
|
||||||
align-items: center;
|
'names names';
|
||||||
* {
|
grid-template-columns: 2fr 1fr;
|
||||||
flex: 1;
|
|
||||||
max-width: 200px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.result-table {
|
||||||
|
grid-area: results;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-chart {
|
||||||
|
grid-area: chart;
|
||||||
|
max-width: 300px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.named-result-table {
|
||||||
|
grid-area: names;
|
||||||
|
}
|
||||||
|
@ -11,10 +11,10 @@ import { MotionRepositoryService } from 'app/core/repositories/motions/motion-re
|
|||||||
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
|
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
|
||||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
import { ChartType } from 'app/shared/components/charts/charts.component';
|
import { ChartType } from 'app/shared/components/charts/charts.component';
|
||||||
|
import { ViewMotion } from 'app/site/motions/models/view-motion';
|
||||||
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||||
import { MotionPollDialogService } from 'app/site/motions/services/motion-poll-dialog.service';
|
import { MotionPollDialogService } from 'app/site/motions/services/motion-poll-dialog.service';
|
||||||
import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-detail.component';
|
import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-detail.component';
|
||||||
// import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'os-motion-poll-detail',
|
selector: 'os-motion-poll-detail',
|
||||||
@ -22,7 +22,7 @@ import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-det
|
|||||||
styleUrls: ['./motion-poll-detail.component.scss']
|
styleUrls: ['./motion-poll-detail.component.scss']
|
||||||
})
|
})
|
||||||
export class MotionPollDetailComponent extends BasePollDetailComponent<ViewMotionPoll> implements OnInit {
|
export class MotionPollDetailComponent extends BasePollDetailComponent<ViewMotionPoll> implements OnInit {
|
||||||
public motionTitle = '';
|
public motion: ViewMotion;
|
||||||
public columnDefinition = ['key', 'value'];
|
public columnDefinition = ['key', 'value'];
|
||||||
|
|
||||||
public set chartType(type: ChartType) {
|
public set chartType(type: ChartType) {
|
||||||
@ -52,11 +52,12 @@ export class MotionPollDetailComponent extends BasePollDetailComponent<ViewMotio
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected onPollLoaded(): void {
|
protected onPollLoaded(): void {
|
||||||
this.motionTitle = this.motionRepo.getViewModel((<ViewMotionPoll>this.poll).motion_id).getTitle();
|
this.motion = this.motionRepo.getViewModel((<ViewMotionPoll>this.poll).motion_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public openDialog(): void {
|
public openDialog(): void {
|
||||||
this.pollDialog.openDialog(this.poll);
|
this.pollDialog.openDialog(this.poll);
|
||||||
|
console.log('this.poll: ', this.poll.hasVotes);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onDeleted(): void {
|
protected onDeleted(): void {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<os-poll-form [data]="pollData" [pollMethods]="motionPollMethods" #pollForm></os-poll-form>
|
<os-poll-form [data]="pollData" #pollForm></os-poll-form>
|
||||||
<ng-container *ngIf="pollForm.contentForm.get('type').value === 'analog'">
|
<ng-container *ngIf="pollForm.contentForm.get('type').value === 'analog'">
|
||||||
<mat-divider></mat-divider>
|
|
||||||
<div class="os-form-card-mobile" mat-dialog-content>
|
<div class="os-form-card-mobile" mat-dialog-content>
|
||||||
<form [formGroup]="dialogVoteForm">
|
<form [formGroup]="dialogVoteForm">
|
||||||
<os-check-input
|
<os-check-input
|
||||||
@ -31,9 +30,7 @@
|
|||||||
></os-check-input>
|
></os-check-input>
|
||||||
<os-check-input
|
<os-check-input
|
||||||
[placeholder]="'Votes invalid' | translate"
|
[placeholder]="'Votes invalid' | translate"
|
||||||
[checkboxValue]="-1"
|
|
||||||
inputType="number"
|
inputType="number"
|
||||||
[checkboxLabel]="'Majority' | translate"
|
|
||||||
formControlName="votesinvalid"
|
formControlName="votesinvalid"
|
||||||
></os-check-input>
|
></os-check-input>
|
||||||
<os-check-input
|
<os-check-input
|
||||||
@ -43,8 +40,7 @@
|
|||||||
></os-check-input>
|
></os-check-input>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<div>
|
||||||
<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>
|
<span translate>Publish immediately</span>
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
@ -53,7 +49,6 @@
|
|||||||
</mat-error>
|
</mat-error>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<mat-divider></mat-divider>
|
|
||||||
<div mat-dialog-actions>
|
<div mat-dialog-actions>
|
||||||
<button mat-button (click)="submitPoll()" [disabled]="pollForm.contentForm.invalid || dialogVoteForm.invalid">
|
<button mat-button (click)="submitPoll()" [disabled]="pollForm.contentForm.invalid || dialogVoteForm.invalid">
|
||||||
<span translate>Save</span>
|
<span translate>Save</span>
|
||||||
|
@ -1,28 +1,21 @@
|
|||||||
<ng-container *ngIf="poll">
|
<ng-container *ngIf="poll">
|
||||||
<ng-container *ngIf="vmanager.canVote(poll)">
|
<ng-container *ngIf="vmanager.canVote(poll)">
|
||||||
<div *ngIf="currentVote"><span translate>Your current vote is</span> '{{ currentVote.valueVerbose | translate }}'</div>
|
|
||||||
<div *ngIf="!currentVote" translate>You have not voted yet.</div>
|
|
||||||
<!-- Voting -->
|
<!-- Voting -->
|
||||||
<mat-radio-group
|
<p *ngFor="let option of voteOptions">
|
||||||
name="votes-{{ poll.id }}"
|
<button
|
||||||
[(ngModel)]="selectedVote"
|
mat-raised-button
|
||||||
|
(click)="saveVote(option.vote)"
|
||||||
|
[ngClass]="currentVote && currentVote.value === option.vote ? option.css : ''"
|
||||||
>
|
>
|
||||||
<mat-radio-button value="Y">
|
<mat-icon> {{ option.icon }}</mat-icon>
|
||||||
<span translate>Yes</span>
|
|
||||||
</mat-radio-button>
|
|
||||||
<mat-radio-button value="N">
|
|
||||||
<span translate>No</span>
|
|
||||||
</mat-radio-button>
|
|
||||||
<mat-radio-button value="A" *ngIf="poll.pollmethod === pollMethods.YNA">
|
|
||||||
<span translate>Abstain</span>
|
|
||||||
</mat-radio-button>
|
|
||||||
</mat-radio-group>
|
|
||||||
<button mat-button (click)="saveVote()">
|
|
||||||
<span translate>Save</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
<span class="vote-label"> {{ option.label | translate }} </span>
|
||||||
|
</p>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="!vmanager.canVote(poll)">
|
<!-- TODO most of the messages are not making sense -->
|
||||||
|
<!-- <ng-container *ngIf="!vmanager.canVote(poll)">
|
||||||
<span>{{ vmanager.getVotePermissionErrorVerbose(poll) | translate }}</span>
|
<span>{{ vmanager.getVotePermissionErrorVerbose(poll) | translate }}</span>
|
||||||
</ng-container>
|
</ng-container> -->
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* These colors should be extracted from some global CSS Constants file
|
||||||
|
*/
|
||||||
|
.voted-yes {
|
||||||
|
background-color: #9fd773;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voted-no {
|
||||||
|
background-color: #cc6c5b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voted-abstain {
|
||||||
|
background-color: #a6a6a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-label {
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
@ -13,21 +13,52 @@ import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
|||||||
import { ViewMotionVote } from 'app/site/motions/models/view-motion-vote';
|
import { ViewMotionVote } from 'app/site/motions/models/view-motion-vote';
|
||||||
import { BasePollVoteComponent } from 'app/site/polls/components/base-poll-vote.component';
|
import { BasePollVoteComponent } from 'app/site/polls/components/base-poll-vote.component';
|
||||||
|
|
||||||
|
interface VoteOption {
|
||||||
|
vote: 'Y' | 'N' | 'A';
|
||||||
|
css: string;
|
||||||
|
icon: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'os-motion-poll-vote',
|
selector: 'os-motion-poll-vote',
|
||||||
templateUrl: './motion-poll-vote.component.html',
|
templateUrl: './motion-poll-vote.component.html',
|
||||||
styleUrls: ['./motion-poll-vote.component.scss']
|
styleUrls: ['./motion-poll-vote.component.scss']
|
||||||
})
|
})
|
||||||
export class MotionPollVoteComponent extends BasePollVoteComponent<ViewMotionPoll> implements OnInit {
|
export class MotionPollVoteComponent extends BasePollVoteComponent<ViewMotionPoll> implements OnInit {
|
||||||
// holds the currently selected vote
|
/**
|
||||||
public selectedVote: 'Y' | 'N' | 'A' = null;
|
* holds the last saved vote
|
||||||
// holds the last saved vote
|
*
|
||||||
|
* TODO: There will be a bug. This has to be reset if the currently observed poll changes it's state back
|
||||||
|
* to started
|
||||||
|
*/
|
||||||
public currentVote: ViewMotionVote;
|
public currentVote: ViewMotionVote;
|
||||||
|
|
||||||
public pollMethods = MotionPollMethods;
|
public pollMethods = MotionPollMethods;
|
||||||
|
|
||||||
private votes: ViewMotionVote[];
|
private votes: ViewMotionVote[];
|
||||||
|
|
||||||
|
public voteOptions: VoteOption[] = [
|
||||||
|
{
|
||||||
|
vote: 'Y',
|
||||||
|
css: 'voted-yes',
|
||||||
|
icon: 'thumb_up',
|
||||||
|
label: 'Yes'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vote: 'N',
|
||||||
|
css: 'voted-no',
|
||||||
|
icon: 'thumb_down',
|
||||||
|
label: 'No'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vote: 'A',
|
||||||
|
css: 'voted-abstain',
|
||||||
|
icon: 'trip_origin',
|
||||||
|
label: 'Abstain'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
title: Title,
|
title: Title,
|
||||||
translate: TranslateService,
|
translate: TranslateService,
|
||||||
@ -51,6 +82,7 @@ export class MotionPollVoteComponent extends BasePollVoteComponent<ViewMotionPol
|
|||||||
|
|
||||||
protected updateVotes(): void {
|
protected updateVotes(): void {
|
||||||
if (this.user && this.votes && this.poll) {
|
if (this.user && this.votes && this.poll) {
|
||||||
|
this.currentVote = null;
|
||||||
const filtered = this.votes.filter(
|
const filtered = this.votes.filter(
|
||||||
vote => vote.option.poll_id === this.poll.id && vote.user_id === this.user.id
|
vote => vote.option.poll_id === this.poll.id && vote.user_id === this.user.id
|
||||||
);
|
);
|
||||||
@ -60,14 +92,14 @@ export class MotionPollVoteComponent extends BasePollVoteComponent<ViewMotionPol
|
|||||||
console.error('A user should never have more than one vote on the same poll.');
|
console.error('A user should never have more than one vote on the same poll.');
|
||||||
}
|
}
|
||||||
this.currentVote = filtered[0];
|
this.currentVote = filtered[0];
|
||||||
this.selectedVote = filtered[0].value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public saveVote(): void {
|
/**
|
||||||
if (this.selectedVote) {
|
* TODO: 'Y' | 'N' | 'A' should refer to some ENUM
|
||||||
this.pollRepo.vote(this.selectedVote, this.poll.id).catch(this.raiseError);
|
*/
|
||||||
}
|
public saveVote(vote: 'Y' | 'N' | 'A'): void {
|
||||||
|
this.pollRepo.vote(vote, this.poll.id).catch(this.raiseError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,61 +1,62 @@
|
|||||||
<div class="poll-preview-wrapper">
|
<div class="poll-preview-wrapper">
|
||||||
|
<!-- Poll Infos -->
|
||||||
<div class="poll-title-wrapper" *ngIf="poll">
|
<div class="poll-title-wrapper" *ngIf="poll">
|
||||||
|
<!-- Title -->
|
||||||
<a class="poll-title" routerLink="/motions/polls/{{ poll.id }}">
|
<a class="poll-title" routerLink="/motions/polls/{{ poll.id }}">
|
||||||
{{ poll.title }}
|
{{ poll.title }}
|
||||||
</a>
|
</a>
|
||||||
<span class="poll-title-actions">
|
|
||||||
|
<!-- Edit button -->
|
||||||
|
<span class="poll-title-actions" *osPerms="'motions.can_manage_polls'">
|
||||||
<button mat-icon-button (click)="openDialog()">
|
<button mat-icon-button (click)="openDialog()">
|
||||||
<mat-icon class="small-icon">edit</mat-icon>
|
<mat-icon class="small-icon">edit</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<div class="poll-properties">
|
|
||||||
<mat-chip *ngIf="pollService.isElectronicVotingEnabled">{{ poll.typeVerbose }}</mat-chip>
|
<!-- State chip -->
|
||||||
|
<div class="poll-properties" *osPerms="'motions.can_manage_polls'">
|
||||||
|
<span *ngIf="pollService.isElectronicVotingEnabled && poll.typeVerbose !== 'Analog'">
|
||||||
|
{{ 'Poll type' | translate }}: {{ poll.typeVerbose | translate }}
|
||||||
|
</span>
|
||||||
|
|
||||||
<mat-chip
|
<mat-chip
|
||||||
|
disableRipple
|
||||||
class="poll-state active"
|
class="poll-state active"
|
||||||
[matMenuTriggerFor]="triggerMenu"
|
[matMenuTriggerFor]="triggerMenu"
|
||||||
[ngClass]="poll.stateVerbose.toLowerCase()"
|
[ngClass]="poll.stateVerbose.toLowerCase()"
|
||||||
>
|
>
|
||||||
{{ poll.stateVerbose }}
|
{{ poll.stateVerbose }}
|
||||||
</mat-chip>
|
</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>
|
||||||
<div class="poll-chart-wrapper" *ngIf="poll">
|
|
||||||
<div *ngIf="poll.type === 'analog' || poll.state === 3 || poll.state === 4" (click)="openPoll()">
|
<!-- Results -->
|
||||||
<ng-container *ngIf="poll.state === 3 || poll.state === 4" [ngTemplateOutlet]="viewTemplate"></ng-container>
|
<ng-container *ngIf="poll && !poll.stateHasVotes && poll.type !== 'analog'; else votingResult">
|
||||||
<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>
|
<os-motion-poll-vote [poll]="poll"></os-motion-poll-vote>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div class="poll-preview-result-wrapper"></div>
|
|
||||||
|
<ng-template #votingResult>
|
||||||
|
<div (click)="openPoll()">
|
||||||
|
<ng-container [ngTemplateOutlet]="poll.hasVotes && poll.stateHasVotes ? viewTemplate : emptyTemplate"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #viewTemplate>
|
<ng-template #viewTemplate>
|
||||||
<div class="chart-wrapper-left">
|
<div class="poll-chart-wrapper">
|
||||||
<mat-icon>close</mat-icon>
|
<div class="votes-yes">
|
||||||
: {{ voteNo }}
|
<os-icon-container icon="check" size="large">
|
||||||
|
{{ voteYes }}
|
||||||
|
</os-icon-container>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="showChart" class="doughnut-chart">
|
<div *ngIf="showChart" class="doughnut-chart">
|
||||||
<os-charts [type]="'doughnut'" [data]="chartDataSubject" [showLegend]="false"> </os-charts>
|
<os-charts [type]="'doughnut'" [data]="chartDataSubject" [showLegend]="false"> </os-charts>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-wrapper-right">
|
<div class="votes-no">
|
||||||
<mat-icon>check</mat-icon>
|
<os-icon-container icon="close" size="large">
|
||||||
: {{ voteYes }}
|
{{ voteNo }}
|
||||||
|
</os-icon-container>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
@ -45,32 +45,19 @@
|
|||||||
|
|
||||||
.poll-chart-wrapper {
|
.poll-chart-wrapper {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 4px auto;
|
display: grid;
|
||||||
display: flex;
|
grid-template-columns: auto minmax(50px, 20%) auto;
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
div {
|
.votes-no {
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-wrapper-left,
|
|
||||||
.chart-wrapper-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-wrapper-left {
|
|
||||||
color: #cc6c5b;
|
color: #cc6c5b;
|
||||||
|
margin: auto 0;
|
||||||
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-wrapper-right {
|
.votes-yes {
|
||||||
color: #9fc773;
|
color: #9fc773;
|
||||||
}
|
margin: auto 0 auto auto;
|
||||||
|
width: fit-content;
|
||||||
.doughnut-chart {
|
|
||||||
max-width: 35%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,6 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
|
|||||||
/**
|
/**
|
||||||
* Number of votes for `Yes`.
|
* Number of votes for `Yes`.
|
||||||
*/
|
*/
|
||||||
// public voteYes = 0;
|
|
||||||
public set voteYes(n: number | string) {
|
public set voteYes(n: number | string) {
|
||||||
this._voteYes = n;
|
this._voteYes = n;
|
||||||
}
|
}
|
||||||
|
@ -14,12 +14,6 @@ import { BasePollRepositoryService } from '../services/base-poll-repository.serv
|
|||||||
import { ViewBasePoll } from '../models/view-base-poll';
|
import { ViewBasePoll } from '../models/view-base-poll';
|
||||||
|
|
||||||
export abstract class BasePollComponent<V extends ViewBasePoll> extends BaseViewComponent {
|
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([]);
|
public chartDataSubject: BehaviorSubject<ChartData> = new BehaviorSubject([]);
|
||||||
|
|
||||||
protected _poll: V;
|
protected _poll: V;
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
[inputListValues]="groupObservable"
|
[inputListValues]="groupObservable"
|
||||||
></os-search-value-selector>
|
></os-search-value-selector>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field>
|
<mat-form-field *ngIf="pollMethods">
|
||||||
<mat-select [placeholder]="'Poll method' | translate" formControlName="pollmethod" required>
|
<mat-select [placeholder]="'Poll method' | translate" formControlName="pollmethod" required>
|
||||||
<mat-option *ngFor="let option of pollMethods | keyvalue" [value]="option.key">
|
<mat-option *ngFor="let option of pollMethods | keyvalue" [value]="option.key">
|
||||||
{{ option.value }}
|
{{ option.value }}
|
||||||
|
Loading…
Reference in New Issue
Block a user