Enhance voting ux
This commit is contained in:
parent
7ab5346198
commit
604df9d48b
@ -13,6 +13,9 @@ export enum VotingError {
|
||||
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 = {
|
||||
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.",
|
||||
|
@ -1,17 +1,14 @@
|
||||
<ol class="breadcrumb-list">
|
||||
<li *ngFor="let breadcrumb of breadcrumbList" class="breadcrumb" [ngClass]="{ active: breadcrumb.active }">
|
||||
<ng-container *ngIf="breadcrumb.active">
|
||||
<div>
|
||||
<mat-button-toggle-group>
|
||||
<mat-button-toggle
|
||||
*ngFor="let breadcrumb of breadcrumbList"
|
||||
[disabled]="breadcrumb.action === null"
|
||||
(click)="breadcrumb.action ? breadcrumb.action() : null"
|
||||
[ngClass]="{ 'active-breadcrumb': breadcrumb.active }"
|
||||
>
|
||||
<span>
|
||||
{{ breadcrumb.label }}
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!breadcrumb.active">
|
||||
<span
|
||||
(click)="breadcrumb.action ? breadcrumb.action() : null"
|
||||
[ngClass]="{ 'accent-foreground has-action': breadcrumb.action }"
|
||||
>
|
||||
{{ breadcrumb.label }}
|
||||
</span>
|
||||
</ng-container>
|
||||
</li>
|
||||
</ol>
|
||||
</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</div>
|
||||
|
@ -1,25 +1,4 @@
|
||||
$breadcrumb-content: var(--breadcrumb-content);
|
||||
|
||||
.breadcrumb-list {
|
||||
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;
|
||||
}
|
||||
.active-breadcrumb {
|
||||
// Theme
|
||||
color: rgba($color: #317796, $alpha: 1);
|
||||
}
|
||||
|
@ -23,6 +23,8 @@ export class BreadcrumbComponent implements OnInit {
|
||||
@Input()
|
||||
public set breadcrumbs(labels: string[] | Breadcrumb[]) {
|
||||
this.breadcrumbList = [];
|
||||
|
||||
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-3.html#caveats
|
||||
for (const breadcrumb of labels) {
|
||||
if (typeof breadcrumb === 'string') {
|
||||
this.breadcrumbList.push({ label: breadcrumb, action: null });
|
||||
@ -45,16 +47,6 @@ export class BreadcrumbComponent implements OnInit {
|
||||
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.
|
||||
*/
|
||||
@ -63,9 +55,7 @@ export class BreadcrumbComponent implements OnInit {
|
||||
/**
|
||||
* Default constructor.
|
||||
*/
|
||||
public constructor() {
|
||||
this.breadcrumbStyle = '/';
|
||||
}
|
||||
public constructor() {}
|
||||
|
||||
/**
|
||||
* OnInit.
|
||||
|
@ -15,6 +15,7 @@
|
||||
<canvas
|
||||
*ngIf="type === 'pie' || type === 'doughnut'"
|
||||
baseChart
|
||||
[options]="pieChartOptions"
|
||||
[data]="circleData"
|
||||
[labels]="circleLabels"
|
||||
[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`.
|
||||
*/
|
||||
|
@ -14,6 +14,7 @@
|
||||
[name]="'checkbox'"
|
||||
[ngModel]="isChecked"
|
||||
(change)="checkboxStateChanged($event.checked)"
|
||||
tabindex="-1"
|
||||
>
|
||||
{{ checkboxLabel }}
|
||||
</mat-checkbox>
|
||||
|
@ -52,6 +52,13 @@ export abstract class BasePoll<T = any, O extends BaseOption<any> = any> extends
|
||||
public onehundred_percent_base: PercentBase;
|
||||
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>)[] {
|
||||
return ['votesvalid', 'votesinvalid', 'votescast'];
|
||||
}
|
||||
|
@ -26,7 +26,7 @@
|
||||
<ng-container *ngIf="isReady">
|
||||
<h1>{{ poll.title }}</h1>
|
||||
<mat-divider></mat-divider>
|
||||
<os-breadcrumb [breadcrumbs]="breadcrumbs" [breadcrumbStyle]="'>'"></os-breadcrumb>
|
||||
<os-breadcrumb [breadcrumbs]="breadcrumbs"></os-breadcrumb>
|
||||
<div class="poll-content">
|
||||
<div>{{ 'Current state' | translate }}: {{ poll.stateVerbose | translate }}</div>
|
||||
<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 voteKeys = ['votesvalid', 'votesinvalid', 'votescast'];
|
||||
|
||||
public get hasVotes(): boolean {
|
||||
return !!this.options[0].votes.length;
|
||||
}
|
||||
|
||||
public initChartLabels(): string[] {
|
||||
return ['Votes'];
|
||||
}
|
||||
|
@ -1,13 +1,6 @@
|
||||
<os-head-bar
|
||||
[goBack]="true"
|
||||
[nav]="false"
|
||||
[hasMainButton]="poll ? poll.state === 2 || poll.state === 3 : false"
|
||||
[mainButtonIcon]="'edit'"
|
||||
[mainActionTooltip]="'Edit' | translate"
|
||||
(mainEvent)="openDialog()"
|
||||
>
|
||||
<os-head-bar [goBack]="true" [nav]="false">
|
||||
<div class="title-slot">
|
||||
<h2 *ngIf="!!poll">{{ motionTitle }}</h2>
|
||||
<h2 *ngIf="motion">{{ 'Motion' | translate }} {{ motion.id }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="menu-slot" *osPerms="'agenda.can_manage'; or: 'agenda.can_see_list_of_speakers'">
|
||||
@ -25,54 +18,61 @@
|
||||
<ng-template #viewTemplate>
|
||||
<ng-container *ngIf="poll">
|
||||
<h1>{{ poll.title }}</h1>
|
||||
<mat-divider></mat-divider>
|
||||
<os-breadcrumb [breadcrumbs]="breadcrumbs" [breadcrumbStyle]="'>'"></os-breadcrumb>
|
||||
<span *ngIf="poll.type !== 'analog'">{{ 'Polly type' | translate }}: {{ poll.type | translate }}</span>
|
||||
<os-breadcrumb [breadcrumbs]="breadcrumbs"></os-breadcrumb>
|
||||
|
||||
<div *ngIf="poll.state === 3 || poll.state === 4">
|
||||
<div *ngIf="poll.stateHasVotes">
|
||||
<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 *ngIf="!poll.hasVotes">{{ 'No results to show' | translate }}</div>
|
||||
|
||||
<div class="result-wrapper" *ngIf="poll.hasVotes">
|
||||
<!-- Chart -->
|
||||
<os-charts
|
||||
class="result-chart"
|
||||
*ngIf="chartDataSubject.value"
|
||||
[type]="chartType"
|
||||
[showLegend]="true"
|
||||
[data]="chartDataSubject"
|
||||
></os-charts>
|
||||
|
||||
<!-- result table -->
|
||||
<mat-table class="result-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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<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 class="poll-content small">
|
||||
<div *ngIf="poll.groups && poll.type && poll.type !== 'analog'">
|
||||
{{ 'Groups' | translate }}:
|
||||
|
||||
<span *ngFor="let group of poll.groups; let i = index">
|
||||
{{ group.getTitle() | translate }}
|
||||
<span *ngIf="i < poll.groups.length - 1">, </span>
|
||||
{{ 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>
|
||||
@ -82,13 +82,17 @@
|
||||
<!-- More Menu -->
|
||||
<mat-menu #pollDetailMenu="matMenu">
|
||||
<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()">
|
||||
<mat-icon>questionmark</mat-icon>
|
||||
<span translate>Pseudoanonymize</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button mat-menu-item (click)="deletePoll()">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<mat-icon color="warn">delete</mat-icon>
|
||||
<span translate>Delete</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
@ -1,18 +1,38 @@
|
||||
@import '~assets/styles/variables.scss';
|
||||
|
||||
.poll-content {
|
||||
padding-top: 10px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
.result-wrapper {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
grid-template-areas:
|
||||
'chart'
|
||||
'results'
|
||||
'names';
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
* {
|
||||
flex: 1;
|
||||
max-width: 200px;
|
||||
@include desktop {
|
||||
.result-wrapper {
|
||||
grid-template-areas:
|
||||
'results chart'
|
||||
'names names';
|
||||
grid-template-columns: 2fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.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 { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
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 { 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',
|
||||
@ -22,7 +22,7 @@ import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-det
|
||||
styleUrls: ['./motion-poll-detail.component.scss']
|
||||
})
|
||||
export class MotionPollDetailComponent extends BasePollDetailComponent<ViewMotionPoll> implements OnInit {
|
||||
public motionTitle = '';
|
||||
public motion: ViewMotion;
|
||||
public columnDefinition = ['key', 'value'];
|
||||
|
||||
public set chartType(type: ChartType) {
|
||||
@ -52,11 +52,12 @@ export class MotionPollDetailComponent extends BasePollDetailComponent<ViewMotio
|
||||
}
|
||||
|
||||
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 {
|
||||
this.pollDialog.openDialog(this.poll);
|
||||
console.log('this.poll: ', this.poll.hasVotes);
|
||||
}
|
||||
|
||||
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'">
|
||||
<mat-divider></mat-divider>
|
||||
<div class="os-form-card-mobile" mat-dialog-content>
|
||||
<form [formGroup]="dialogVoteForm">
|
||||
<os-check-input
|
||||
@ -31,9 +30,7 @@
|
||||
></os-check-input>
|
||||
<os-check-input
|
||||
[placeholder]="'Votes invalid' | translate"
|
||||
[checkboxValue]="-1"
|
||||
inputType="number"
|
||||
[checkboxLabel]="'Majority' | translate"
|
||||
formControlName="votesinvalid"
|
||||
></os-check-input>
|
||||
<os-check-input
|
||||
@ -43,8 +40,7 @@
|
||||
></os-check-input>
|
||||
</form>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<div class="spacer-top-20">
|
||||
<div>
|
||||
<mat-checkbox [(ngModel)]="publishImmediately" (change)="publishStateChanged($event.checked)">
|
||||
<span translate>Publish immediately</span>
|
||||
</mat-checkbox>
|
||||
@ -53,7 +49,6 @@
|
||||
</mat-error>
|
||||
</div>
|
||||
</ng-container>
|
||||
<mat-divider></mat-divider>
|
||||
<div mat-dialog-actions>
|
||||
<button mat-button (click)="submitPoll()" [disabled]="pollForm.contentForm.invalid || dialogVoteForm.invalid">
|
||||
<span translate>Save</span>
|
||||
|
@ -1,28 +1,21 @@
|
||||
<ng-container *ngIf="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 -->
|
||||
<mat-radio-group
|
||||
name="votes-{{ poll.id }}"
|
||||
[(ngModel)]="selectedVote"
|
||||
>
|
||||
<mat-radio-button value="Y">
|
||||
<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>
|
||||
<p *ngFor="let option of voteOptions">
|
||||
<button
|
||||
mat-raised-button
|
||||
(click)="saveVote(option.vote)"
|
||||
[ngClass]="currentVote && currentVote.value === option.vote ? option.css : ''"
|
||||
>
|
||||
<mat-icon> {{ option.icon }}</mat-icon>
|
||||
</button>
|
||||
<span class="vote-label"> {{ option.label | translate }} </span>
|
||||
</p>
|
||||
</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>
|
||||
</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 { BasePollVoteComponent } from 'app/site/polls/components/base-poll-vote.component';
|
||||
|
||||
interface VoteOption {
|
||||
vote: 'Y' | 'N' | 'A';
|
||||
css: string;
|
||||
icon: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'os-motion-poll-vote',
|
||||
templateUrl: './motion-poll-vote.component.html',
|
||||
styleUrls: ['./motion-poll-vote.component.scss']
|
||||
})
|
||||
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 pollMethods = MotionPollMethods;
|
||||
|
||||
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(
|
||||
title: Title,
|
||||
translate: TranslateService,
|
||||
@ -51,6 +82,7 @@ export class MotionPollVoteComponent extends BasePollVoteComponent<ViewMotionPol
|
||||
|
||||
protected updateVotes(): void {
|
||||
if (this.user && this.votes && this.poll) {
|
||||
this.currentVote = null;
|
||||
const filtered = this.votes.filter(
|
||||
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.');
|
||||
}
|
||||
this.currentVote = filtered[0];
|
||||
this.selectedVote = filtered[0].value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public saveVote(): void {
|
||||
if (this.selectedVote) {
|
||||
this.pollRepo.vote(this.selectedVote, this.poll.id).catch(this.raiseError);
|
||||
}
|
||||
/**
|
||||
* TODO: 'Y' | 'N' | 'A' should refer to some ENUM
|
||||
*/
|
||||
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">
|
||||
<!-- Poll Infos -->
|
||||
<div class="poll-title-wrapper" *ngIf="poll">
|
||||
<!-- Title -->
|
||||
<a class="poll-title" routerLink="/motions/polls/{{ poll.id }}">
|
||||
{{ poll.title }}
|
||||
</a>
|
||||
<span class="poll-title-actions">
|
||||
|
||||
<!-- Edit button -->
|
||||
<span class="poll-title-actions" *osPerms="'motions.can_manage_polls'">
|
||||
<button mat-icon-button (click)="openDialog()">
|
||||
<mat-icon class="small-icon">edit</mat-icon>
|
||||
</button>
|
||||
</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
|
||||
disableRipple
|
||||
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 class="poll-chart-wrapper" *ngIf="poll">
|
||||
<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>
|
||||
</div>
|
||||
<div class="poll-preview-result-wrapper"></div>
|
||||
|
||||
<!-- Results -->
|
||||
<ng-container *ngIf="poll && !poll.stateHasVotes && poll.type !== 'analog'; else votingResult">
|
||||
<os-motion-poll-vote [poll]="poll"></os-motion-poll-vote>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<ng-template #votingResult>
|
||||
<div (click)="openPoll()">
|
||||
<ng-container [ngTemplateOutlet]="poll.hasVotes && poll.stateHasVotes ? viewTemplate : emptyTemplate"></ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #viewTemplate>
|
||||
<div class="chart-wrapper-left">
|
||||
<mat-icon>close</mat-icon>
|
||||
: {{ voteNo }}
|
||||
</div>
|
||||
<div *ngIf="showChart" class="doughnut-chart">
|
||||
<os-charts [type]="'doughnut'" [data]="chartDataSubject" [showLegend]="false"> </os-charts>
|
||||
</div>
|
||||
<div class="chart-wrapper-right">
|
||||
<mat-icon>check</mat-icon>
|
||||
: {{ voteYes }}
|
||||
<div class="poll-chart-wrapper">
|
||||
<div class="votes-yes">
|
||||
<os-icon-container icon="check" size="large">
|
||||
{{ voteYes }}
|
||||
</os-icon-container>
|
||||
</div>
|
||||
<div *ngIf="showChart" class="doughnut-chart">
|
||||
<os-charts [type]="'doughnut'" [data]="chartDataSubject" [showLegend]="false"> </os-charts>
|
||||
</div>
|
||||
<div class="votes-no">
|
||||
<os-icon-container icon="close" size="large">
|
||||
{{ voteNo }}
|
||||
</os-icon-container>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
|
@ -45,32 +45,19 @@
|
||||
|
||||
.poll-chart-wrapper {
|
||||
cursor: pointer;
|
||||
margin: 4px auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(50px, 20%) auto;
|
||||
|
||||
div {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chart-wrapper-left,
|
||||
.chart-wrapper-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.chart-wrapper-left {
|
||||
.votes-no {
|
||||
color: #cc6c5b;
|
||||
margin: auto 0;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.chart-wrapper-right {
|
||||
.votes-yes {
|
||||
color: #9fc773;
|
||||
}
|
||||
|
||||
.doughnut-chart {
|
||||
max-width: 35%;
|
||||
margin: auto 0 auto auto;
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +56,6 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
|
||||
/**
|
||||
* Number of votes for `Yes`.
|
||||
*/
|
||||
// public voteYes = 0;
|
||||
public set voteYes(n: number | string) {
|
||||
this._voteYes = n;
|
||||
}
|
||||
|
@ -14,12 +14,6 @@ import { BasePollRepositoryService } from '../services/base-poll-repository.serv
|
||||
import { ViewBasePoll } from '../models/view-base-poll';
|
||||
|
||||
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;
|
||||
|
@ -36,7 +36,7 @@
|
||||
[inputListValues]="groupObservable"
|
||||
></os-search-value-selector>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-form-field *ngIf="pollMethods">
|
||||
<mat-select [placeholder]="'Poll method' | translate" formControlName="pollmethod" required>
|
||||
<mat-option *ngFor="let option of pollMethods | keyvalue" [value]="option.key">
|
||||
{{ option.value }}
|
||||
|
Loading…
Reference in New Issue
Block a user