Enhance voting ux

This commit is contained in:
Sean 2020-01-22 11:57:51 +01:00 committed by FinnStutzenstein
parent 7ab5346198
commit 604df9d48b
22 changed files with 249 additions and 216 deletions

View File

@ -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.",

View File

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

View File

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

View File

@ -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.

View File

@ -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"

View File

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

View File

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

View File

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

View File

@ -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'">

View File

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

View File

@ -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,54 +18,61 @@
<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>
<os-charts
*ngIf="chartDataSubject.value" <div *ngIf="!poll.hasVotes">{{ 'No results to show' | translate }}</div>
[type]="chartType"
[showLegend]="true" <div class="result-wrapper" *ngIf="poll.hasVotes">
[data]="chartDataSubject" <!-- Chart -->
></os-charts> <os-charts
<div class="result-chart"
*ngIf="poll.type === 'named'" *ngIf="chartDataSubject.value"
style="display: grid; grid-template-columns: max-content auto;grid-column-gap: 20px;" [type]="chartType"
> [showLegend]="true"
<ng-container *ngFor="let vote of poll.options[0].votes"> [data]="chartDataSubject"
<div *ngIf="vote.user">{{ vote.user.full_name }}</div> ></os-charts>
<div *ngIf="!vote.user">{{ 'Unknown user' | translate }}</div>
<div>{{ vote.valueVerbose }}</div> <!-- result table -->
</ng-container> <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> </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>
<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>

View File

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

View File

@ -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 {

View File

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

View File

@ -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)"
<mat-radio-button value="Y"> [ngClass]="currentVote && currentVote.value === option.vote ? option.css : ''"
<span translate>Yes</span> >
</mat-radio-button> <mat-icon> {{ option.icon }}</mat-icon>
<mat-radio-button value="N"> </button>
<span translate>No</span> <span class="vote-label"> {{ option.label | translate }} </span>
</mat-radio-button> </p>
<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>
</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>

View File

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

View File

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

View File

@ -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 <os-motion-poll-vote [poll]="poll"></os-motion-poll-vote>
*ngIf="(poll.state === 1 || poll.state === 2) && poll.type === 'analog'" </ng-container>
[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>
</div> </div>
<ng-template #votingResult>
<div (click)="openPoll()">
<ng-container [ngTemplateOutlet]="poll.hasVotes && poll.stateHasVotes ? viewTemplate : emptyTemplate"></ng-container>
</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">
</div> {{ voteYes }}
<div *ngIf="showChart" class="doughnut-chart"> </os-icon-container>
<os-charts [type]="'doughnut'" [data]="chartDataSubject" [showLegend]="false"> </os-charts> </div>
</div> <div *ngIf="showChart" class="doughnut-chart">
<div class="chart-wrapper-right"> <os-charts [type]="'doughnut'" [data]="chartDataSubject" [showLegend]="false"> </os-charts>
<mat-icon>check</mat-icon> </div>
: {{ voteYes }} <div class="votes-no">
<os-icon-container icon="close" size="large">
{{ voteNo }}
</os-icon-container>
</div>
</div> </div>
</ng-template> </ng-template>

View File

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

View File

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

View File

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

View File

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