Even more voting refinement
Various additional refinements for a more well rounded voting experience
This commit is contained in:
parent
a05662a0f8
commit
ee4c6aa0bf
@ -1,4 +1,4 @@
|
||||
@import '~assets/styles/poll-colors.scss';
|
||||
@import '~assets/styles/poll-styles-common.scss';
|
||||
|
||||
.result-wrapper {
|
||||
display: grid;
|
||||
@ -21,18 +21,6 @@
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.yes {
|
||||
color: $votes-yes-color;
|
||||
}
|
||||
|
||||
.no {
|
||||
color: $votes-no-color;
|
||||
}
|
||||
|
||||
.abstain {
|
||||
color: $votes-abstain-color;
|
||||
}
|
||||
}
|
||||
|
||||
.doughnut-chart {
|
||||
|
@ -77,6 +77,22 @@ export abstract class BasePoll<
|
||||
return this.onehundred_percent_base === PercentBase.Cast;
|
||||
}
|
||||
|
||||
public get isAnalog(): boolean {
|
||||
return this.type === PollType.Analog;
|
||||
}
|
||||
|
||||
public get isNamed(): boolean {
|
||||
return this.type === PollType.Named;
|
||||
}
|
||||
|
||||
public get isAnon(): boolean {
|
||||
return this.type === PollType.Pseudoanonymous;
|
||||
}
|
||||
|
||||
public get isEVoting(): boolean {
|
||||
return this.isNamed || this.isAnon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the state is finished or published
|
||||
*/
|
||||
|
@ -36,12 +36,10 @@ export class PollPercentBasePipe implements PipeTransform {
|
||||
totalByBase = this.motionPollService.getPercentBase(poll);
|
||||
}
|
||||
|
||||
if (totalByBase) {
|
||||
if (totalByBase && totalByBase > 0) {
|
||||
const percentNumber = (value / totalByBase) * 100;
|
||||
if (percentNumber > 0) {
|
||||
const result = percentNumber % 1 === 0 ? percentNumber : percentNumber.toFixed(this.decimalPlaces);
|
||||
return `(${result} %)`;
|
||||
}
|
||||
const result = percentNumber % 1 === 0 ? percentNumber : percentNumber.toFixed(this.decimalPlaces);
|
||||
return `(${result} %)`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -263,7 +263,7 @@
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
placeholder="{{ 'Default comment on the ballot paper' | translate }}"
|
||||
placeholder="{{ 'Hint on voting' | translate }}"
|
||||
formControlName="default_poll_description"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
@ -9,4 +9,21 @@
|
||||
border-bottom: 1px solid mat-color($background, focused-button);
|
||||
}
|
||||
}
|
||||
|
||||
.openslides-theme .pbl-ngrid-row:hover {
|
||||
background-color: mat-color($background, card);
|
||||
}
|
||||
|
||||
.openslides-theme os-list-view-table os-sort-filter-bar .custom-table-header {
|
||||
&,
|
||||
.action-buttons .input-container input {
|
||||
background: mat-color($background, card);
|
||||
}
|
||||
}
|
||||
|
||||
.openslides-theme .pbl-ngrid-header-cell:first-child {
|
||||
&::after {
|
||||
border-right: 1px solid mat-color($background, focused-button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<th class="voting-option" translate>Candidates</th>
|
||||
<th class="result voted-yes">
|
||||
<th class="result yes">
|
||||
<span *ngIf="!poll.isMethodY" translate>
|
||||
Yes
|
||||
</span>
|
||||
@ -35,8 +35,8 @@
|
||||
Votes
|
||||
</span>
|
||||
</th>
|
||||
<th class="result voted-no" translate *ngIf="!poll.isMethodY">No</th>
|
||||
<th class="result voted-abstain" translate *ngIf="poll.isMethodYNA">Abstain</th>
|
||||
<th class="result no" translate *ngIf="!poll.isMethodY">No</th>
|
||||
<th class="result abstain" translate *ngIf="poll.isMethodYNA">Abstain</th>
|
||||
</tr>
|
||||
<tr *ngFor="let row of getTableData()" [class]="row.class">
|
||||
<td class="voting-option">
|
||||
|
@ -1,5 +1,5 @@
|
||||
@import '~assets/styles/variables.scss';
|
||||
@import '~assets/styles/poll-colors.scss';
|
||||
@import '~assets/styles/poll-styles-common.scss';
|
||||
|
||||
.assignment-result-wrapper {
|
||||
.assignment-result-table {
|
||||
@ -88,30 +88,6 @@
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.voted-yes {
|
||||
color: $votes-yes-color;
|
||||
}
|
||||
|
||||
.voted-no {
|
||||
color: $votes-no-color;
|
||||
}
|
||||
|
||||
.voted-abstain {
|
||||
color: $votes-abstain-color;
|
||||
}
|
||||
|
||||
// theme
|
||||
.openslides-theme .pbl-ngrid-row:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.openslides-theme os-list-view-table os-sort-filter-bar .custom-table-header {
|
||||
&,
|
||||
.action-buttons .input-container input {
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
|
||||
.openslides-theme .pbl-ngrid-no-data {
|
||||
top: 10%;
|
||||
}
|
||||
@ -128,6 +104,5 @@
|
||||
top: 0;
|
||||
right: -1px;
|
||||
height: 100%;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Component, ViewEncapsulation } 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 { PblColumnDefinition } from '@pebula/ngrid';
|
||||
@ -49,7 +49,8 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
||||
pollService: PollService,
|
||||
votesRepo: AssignmentVoteRepositoryService,
|
||||
private operator: OperatorService,
|
||||
private assignmentPollService: AssignmentPollService
|
||||
private assignmentPollService: AssignmentPollService,
|
||||
private router: Router
|
||||
) {
|
||||
super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog, pollService, votesRepo);
|
||||
}
|
||||
@ -126,8 +127,7 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
||||
}
|
||||
|
||||
public getVoteClass(votingResult: VotingResult): string {
|
||||
const cssPrefix = 'voted-';
|
||||
return `${cssPrefix}${votingResult.vote}`;
|
||||
return votingResult.vote;
|
||||
}
|
||||
|
||||
public voteFitsMethod(result: VotingResult): boolean {
|
||||
@ -143,6 +143,10 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
||||
return true;
|
||||
}
|
||||
|
||||
protected onDeleted(): void {
|
||||
this.router.navigate(['assignments', this.poll.assignment_id]);
|
||||
}
|
||||
|
||||
public getTableData(): PollTableData[] {
|
||||
return this.assignmentPollService.generateTableData(this.poll);
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
<ng-container *ngIf="poll">
|
||||
<ng-container *ngIf="vmanager.canVote(poll) && !alreadyVoted; else cannotVote">
|
||||
<!-- Submit Vote -->
|
||||
<ng-container [ngTemplateOutlet]="sendNow"></ng-container>
|
||||
<!-- Poll hint -->
|
||||
<p *ngIf="pollHint">
|
||||
<i>{{ pollHint }}</i>
|
||||
</p>
|
||||
|
||||
<!-- Leftover votes -->
|
||||
<h4
|
||||
*ngIf="poll.pollmethod === AssignmentPollMethod.Votes && poll.votes_amount > 1 && !isGlobalOptionSelected()"
|
||||
*ngIf="poll.pollmethod === AssignmentPollMethod.Votes && poll.votes_amount > 1"
|
||||
>
|
||||
{{ 'Votes for this poll' | translate }}: {{ getVotesCount() }}/{{ poll.votes_amount }}
|
||||
</h4>
|
||||
@ -32,10 +34,12 @@
|
||||
|
||||
<div *ngFor="let action of voteActions">
|
||||
<button
|
||||
class="vote-button"
|
||||
mat-raised-button
|
||||
(click)="saveSingleVote(option.id, action.vote)"
|
||||
[ngClass]="
|
||||
voteRequestData.votes[option.id] === action.vote || voteRequestData.votes[option.id] === 1
|
||||
voteRequestData.votes[option.id] === action.vote ||
|
||||
voteRequestData.votes[option.id] === 1
|
||||
? action.css
|
||||
: ''
|
||||
"
|
||||
@ -57,6 +61,7 @@
|
||||
<div class="global-option-grid">
|
||||
<div *ngIf="poll.global_no">
|
||||
<button
|
||||
class="vote-button"
|
||||
mat-raised-button
|
||||
(click)="saveGlobalVote('N')"
|
||||
[ngClass]="voteRequestData.global === 'N' ? 'voted-no' : ''"
|
||||
@ -95,9 +100,13 @@
|
||||
|
||||
<ng-template #cannotVote>
|
||||
<div class="centered-button-wrapper">
|
||||
<os-icon-container icon="check">
|
||||
{{ 'You already voted on this poll' | translate}}
|
||||
</os-icon-container>
|
||||
<div>
|
||||
<mat-icon class="vote-submitted">
|
||||
check_circle
|
||||
</mat-icon>
|
||||
<br />
|
||||
<span>{{ 'You already voted on this poll.' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
@import '~assets/styles/poll-colors.scss';
|
||||
@import '~assets/styles/poll-styles-common.scss';
|
||||
|
||||
%vote-grid-base {
|
||||
display: grid;
|
||||
@ -42,23 +43,21 @@
|
||||
|
||||
.centered-button-wrapper {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
> * {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.vote-submitted {
|
||||
color: $votes-yes-color;
|
||||
font-size: 200%;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Could be some more general component
|
||||
.voted-yes {
|
||||
background-color: $votes-yes-color;
|
||||
}
|
||||
|
||||
.voted-no {
|
||||
background-color: $votes-no-color;
|
||||
}
|
||||
|
||||
.voted-abstain {
|
||||
background-color: $votes-abstain-color;
|
||||
.vote-button {
|
||||
min-width: 50px;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.vote-label {
|
||||
|
@ -62,6 +62,10 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
|
||||
}
|
||||
}
|
||||
|
||||
public get pollHint(): string {
|
||||
return this.poll.assignment.default_poll_description;
|
||||
}
|
||||
|
||||
private defineVoteOptions(): void {
|
||||
this.voteActions.push({
|
||||
vote: 'Y',
|
||||
@ -93,7 +97,7 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
|
||||
return Object.keys(this.voteRequestData.votes).filter(key => this.voteRequestData.votes[key]).length;
|
||||
}
|
||||
|
||||
public isGlobalOptionSelected(): boolean {
|
||||
private isGlobalOptionSelected(): boolean {
|
||||
return !!this.voteRequestData.global;
|
||||
}
|
||||
|
||||
@ -145,7 +149,7 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
|
||||
}
|
||||
} else {
|
||||
this.raiseError(
|
||||
this.translate.instant('You reached the maximum amount of votes. Deselect somebody first')
|
||||
this.translate.instant('You reached the maximum amount of votes. Deselect somebody first.')
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@ -165,7 +169,11 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
|
||||
|
||||
public saveGlobalVote(globalVote: GlobalVote): void {
|
||||
this.voteRequestData.votes = {};
|
||||
this.voteRequestData.global = globalVote;
|
||||
this.submitVote();
|
||||
if (this.voteRequestData.global && this.voteRequestData.global === globalVote) {
|
||||
delete this.voteRequestData.global;
|
||||
} else {
|
||||
this.voteRequestData.global = globalVote;
|
||||
this.submitVote();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,10 +30,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Change state button -->
|
||||
<div *osPerms="'assignments.can_manage'">
|
||||
<div *osPerms="'assignments.can_manage'; and: !hideChangeState">
|
||||
<button
|
||||
mat-stroked-button
|
||||
*ngIf="!poll.isPublished"
|
||||
[ngClass]="pollStateActions[poll.state].css"
|
||||
(click)="changeState(poll.nextState)"
|
||||
>
|
||||
@ -43,6 +42,11 @@
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Enter Votes Hint -->
|
||||
<div *osPerms="'assignments.can_manage'; and: poll.type === 'analog' && !poll.stateHasVotes">
|
||||
{{ 'Edit to enter votes.' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart -->
|
||||
@ -70,9 +74,18 @@
|
||||
<div *osPerms="'assignments.can_manage'; and: poll && poll.isStarted">
|
||||
<os-poll-progress [poll]="poll"></os-poll-progress>
|
||||
</div>
|
||||
|
||||
<!-- The Vote -->
|
||||
<os-assignment-poll-vote *ngIf="poll.canBeVotedFor" [poll]="poll"></os-assignment-poll-vote>
|
||||
|
||||
<!-- More-Button -->
|
||||
<div class="poll-detail-button-wrapper">
|
||||
<a mat-icon-button routerLink="/assignments/polls/{{ poll.id }}" matTooltip="{{ 'More' | translate }}">
|
||||
<a
|
||||
mat-icon-button
|
||||
routerLink="/assignments/polls/{{ poll.id }}"
|
||||
matTooltip="{{ 'More' | translate }}"
|
||||
*ngIf="poll.isPublished"
|
||||
>
|
||||
<mat-icon class="small-icon">
|
||||
visibility
|
||||
</mat-icon>
|
||||
|
@ -1,4 +1,5 @@
|
||||
@import '~assets/styles/poll-colors.scss';
|
||||
@import '~assets/styles/poll-styles-common.scss';
|
||||
|
||||
.assignment-poll-wrapper {
|
||||
position: relative;
|
||||
@ -17,16 +18,4 @@
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.start-poll-button {
|
||||
color: green !important;
|
||||
}
|
||||
|
||||
.stop-poll-button {
|
||||
color: $poll-stop-color;
|
||||
}
|
||||
|
||||
.publish-poll-button {
|
||||
color: $poll-publish-color;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
@ -21,8 +21,7 @@ import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||
@Component({
|
||||
selector: 'os-assignment-poll',
|
||||
templateUrl: './assignment-poll.component.html',
|
||||
styleUrls: ['./assignment-poll.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
styleUrls: ['./assignment-poll.component.scss']
|
||||
})
|
||||
export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPoll> implements OnInit {
|
||||
@Input()
|
||||
|
@ -190,7 +190,7 @@ export class AssignmentPollPdfService extends PollPdfService {
|
||||
*/
|
||||
private createPollHint(poll: ViewAssignmentPoll): object {
|
||||
return {
|
||||
text: poll.description || '',
|
||||
text: poll.assignment.default_poll_description || '',
|
||||
style: 'description'
|
||||
};
|
||||
}
|
||||
|
@ -32,6 +32,8 @@ export class AssignmentPollService extends PollService {
|
||||
|
||||
public defaultPollMethod: AssignmentPollMethod;
|
||||
|
||||
private sortByVote: boolean;
|
||||
|
||||
/**
|
||||
* Constructor. Subscribes to the configuration values needed
|
||||
* @param config ConfigService
|
||||
@ -53,6 +55,7 @@ export class AssignmentPollService extends PollService {
|
||||
config
|
||||
.get<AssignmentPollMethod>(AssignmentPoll.defaultPollMethodConfig)
|
||||
.subscribe(method => (this.defaultPollMethod = method));
|
||||
config.get<boolean>('assignment_poll_sort_poll_result_by_votes').subscribe(sort => (this.sortByVote = sort));
|
||||
}
|
||||
|
||||
public getDefaultPollData(contextId?: number): AssignmentPoll {
|
||||
@ -74,38 +77,47 @@ export class AssignmentPollService extends PollService {
|
||||
}
|
||||
|
||||
private getGlobalVoteKeys(poll: ViewAssignmentPoll): VotingResult[] {
|
||||
// debugger;
|
||||
return [
|
||||
{
|
||||
vote: 'amount_global_no',
|
||||
showPercent: false,
|
||||
hide: poll.amount_global_no === -2 || poll.amount_global_no === 0
|
||||
showPercent: this.showPercentOfValidOrCast(poll),
|
||||
hide: poll.amount_global_no === -2 || !poll.amount_global_no
|
||||
},
|
||||
{
|
||||
vote: 'amount_global_abstain',
|
||||
showPercent: false,
|
||||
hide: poll.amount_global_abstain === -2 || poll.amount_global_abstain === 0
|
||||
showPercent: this.showPercentOfValidOrCast(poll),
|
||||
hide: poll.amount_global_abstain === -2 || !poll.amount_global_abstain
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
public generateTableData(poll: ViewAssignmentPoll): PollTableData[] {
|
||||
const tableData: PollTableData[] = poll.options.map(candidate => ({
|
||||
votingOption: candidate.user.short_name,
|
||||
votingOptionSubtitle: candidate.user.getLevelAndNumber(),
|
||||
class: 'user',
|
||||
value: super.getVoteTableKeys(poll).map(
|
||||
key =>
|
||||
({
|
||||
vote: key.vote,
|
||||
amount: candidate[key.vote],
|
||||
icon: key.icon,
|
||||
hide: key.hide,
|
||||
showPercent: key.showPercent
|
||||
} as VotingResult)
|
||||
)
|
||||
}));
|
||||
tableData.push(...this.formatVotingResultToTableData(super.getSumTableKeys(poll), poll));
|
||||
const tableData: PollTableData[] = poll.options
|
||||
.sort((a, b) => {
|
||||
if (this.sortByVote) {
|
||||
return b.yes - a.yes;
|
||||
} else {
|
||||
return b.weight - a.weight;
|
||||
}
|
||||
})
|
||||
.map(candidate => ({
|
||||
votingOption: candidate.user.short_name,
|
||||
votingOptionSubtitle: candidate.user.getLevelAndNumber(),
|
||||
class: 'user',
|
||||
value: super.getVoteTableKeys(poll).map(
|
||||
key =>
|
||||
({
|
||||
vote: key.vote,
|
||||
amount: candidate[key.vote],
|
||||
icon: key.icon,
|
||||
hide: key.hide,
|
||||
showPercent: key.showPercent
|
||||
} as VotingResult)
|
||||
)
|
||||
}));
|
||||
tableData.push(...this.formatVotingResultToTableData(this.getGlobalVoteKeys(poll), poll));
|
||||
tableData.push(...this.formatVotingResultToTableData(super.getSumTableKeys(poll), poll));
|
||||
return tableData;
|
||||
}
|
||||
|
||||
|
@ -31,8 +31,9 @@
|
||||
|
||||
<div *ngIf="!poll.hasVotes || !poll.stateHasVotes">{{ 'No results to show' | translate }}</div>
|
||||
|
||||
<div *ngIf="poll.stateHasVotes">
|
||||
<os-motion-poll-detail-content [poll]="poll" [chartData]="chartDataSubject"> </os-motion-poll-detail-content>
|
||||
<div *ngIf="poll.stateHasVotes && (hasPerms() || poll.isPublished)">
|
||||
<os-motion-poll-detail-content [poll]="poll" [chartData]="chartDataSubject">
|
||||
</os-motion-poll-detail-content>
|
||||
|
||||
<!-- Named table: only show if votes are present -->
|
||||
<div class="named-result-table" *ngIf="poll.type === 'named'">
|
||||
|
@ -1,5 +1,4 @@
|
||||
@import '~assets/styles/variables.scss';
|
||||
@import '~assets/styles/poll-colors.scss';
|
||||
@import '~assets/styles/poll-styles-common.scss';
|
||||
|
||||
.poll-content {
|
||||
text-align: right;
|
||||
@ -30,18 +29,6 @@
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.voted-yes {
|
||||
color: $votes-yes-color;
|
||||
}
|
||||
|
||||
.voted-no {
|
||||
color: $votes-no-color;
|
||||
}
|
||||
|
||||
.voted-abstain {
|
||||
color: $votes-abstain-color;
|
||||
}
|
||||
|
||||
.openslides-theme .pbl-ngrid-no-data {
|
||||
top: 10%;
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
<div *osPerms="'motions.can_manage_polls'; and: poll && poll.isStarted">
|
||||
<os-poll-progress [poll]="poll"></os-poll-progress>
|
||||
</div>
|
||||
<ng-container *ngIf="poll && !poll.user_has_voted; else userHasVotes">
|
||||
<div *osPerms="'motions.can_manage_polls'; and: poll && poll.isStarted">
|
||||
<os-poll-progress [poll]="poll"></os-poll-progress>
|
||||
</div>
|
||||
<div *ngIf="vmanager.canVote(poll)" class="vote-button-grid">
|
||||
<!-- Voting -->
|
||||
<div class="vote-button" *ngFor="let option of voteOptions">
|
||||
<button
|
||||
mat-raised-button
|
||||
(click)="saveVote(option.vote)"
|
||||
[ngClass]="currentVote && currentVote.value === option.vote ? option.css : ''"
|
||||
[ngClass]="currentVote && currentVote.vote === option.vote ? option.css : ''"
|
||||
>
|
||||
<mat-icon> {{ option.icon }}</mat-icon>
|
||||
</button>
|
||||
@ -19,8 +19,12 @@
|
||||
|
||||
<ng-template #userHasVotes>
|
||||
<div class="user-has-voted">
|
||||
<os-icon-container icon="check">
|
||||
{{ 'You already voted on this poll' | translate }}
|
||||
</os-icon-container>
|
||||
<div>
|
||||
<mat-icon class="vote-submitted">
|
||||
check_circle
|
||||
</mat-icon>
|
||||
<br />
|
||||
<span>{{ 'You already voted on this poll.' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
@ -1,4 +1,5 @@
|
||||
@import '~assets/styles/poll-colors.scss';
|
||||
@import '~assets/styles/poll-styles-common.scss';
|
||||
|
||||
.vote-button-grid {
|
||||
display: grid;
|
||||
@ -19,24 +20,15 @@
|
||||
|
||||
.user-has-voted {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
> * {
|
||||
margin-top: 1em;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.voted-yes {
|
||||
background-color: $votes-yes-color;
|
||||
color: $vote-active-color;
|
||||
}
|
||||
|
||||
.voted-no {
|
||||
background-color: $votes-no-color;
|
||||
color: $vote-active-color;
|
||||
}
|
||||
|
||||
.voted-abstain {
|
||||
background-color: $votes-abstain-color;
|
||||
color: $vote-active-color;
|
||||
.vote-submitted {
|
||||
color: $votes-yes-color;
|
||||
font-size: 200%;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
@ -6,20 +6,16 @@ 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 { MotionVoteRepositoryService } from 'app/core/repositories/motions/motion-vote-repository.service';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { VotingService } from 'app/core/ui-services/voting.service';
|
||||
import { MotionPollMethod } from 'app/shared/models/motions/motion-poll';
|
||||
import { PollType } from 'app/shared/models/poll/base-poll';
|
||||
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;
|
||||
vote?: 'Y' | 'N' | 'A';
|
||||
css?: string;
|
||||
icon?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@ -27,19 +23,8 @@ interface VoteOption {
|
||||
templateUrl: './motion-poll-vote.component.html',
|
||||
styleUrls: ['./motion-poll-vote.component.scss']
|
||||
})
|
||||
export class MotionPollVoteComponent extends BasePollVoteComponent<ViewMotionPoll> implements OnInit {
|
||||
/**
|
||||
* 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 MotionPollMethod = MotionPollMethod;
|
||||
|
||||
private votes: ViewMotionVote[];
|
||||
|
||||
export class MotionPollVoteComponent extends BasePollVoteComponent<ViewMotionPoll> {
|
||||
public currentVote: VoteOption = {};
|
||||
public voteOptions: VoteOption[] = [
|
||||
{
|
||||
vote: 'Y',
|
||||
@ -67,52 +52,23 @@ export class MotionPollVoteComponent extends BasePollVoteComponent<ViewMotionPol
|
||||
matSnackbar: MatSnackBar,
|
||||
vmanager: VotingService,
|
||||
operator: OperatorService,
|
||||
private voteRepo: MotionVoteRepositoryService,
|
||||
private pollRepo: MotionPollRepositoryService,
|
||||
private promptService: PromptService
|
||||
) {
|
||||
super(title, translate, matSnackbar, vmanager, operator);
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.subscriptions.push(
|
||||
this.voteRepo.getViewModelListObservable().subscribe(votes => {
|
||||
this.votes = votes;
|
||||
this.updateVotes();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
if (filtered.length) {
|
||||
if (filtered.length > 1) {
|
||||
// output warning and continue to keep the error case user friendly
|
||||
console.error('A user should never have more than one vote on the same poll.');
|
||||
}
|
||||
this.currentVote = filtered[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: 'Y' | 'N' | 'A' should refer to some ENUM
|
||||
*/
|
||||
public saveVote(vote: 'Y' | 'N' | 'A'): void {
|
||||
if (this.poll.type === PollType.Pseudoanonymous) {
|
||||
const title = this.translate.instant('Are you sure?');
|
||||
const content = this.translate.instant('Your decision cannot be changed afterwards');
|
||||
this.promptService.open(title, content).then(confirmed => {
|
||||
if (confirmed) {
|
||||
this.pollRepo.vote(vote, this.poll.id).catch(this.raiseError);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.pollRepo.vote(vote, this.poll.id).catch(this.raiseError);
|
||||
}
|
||||
this.currentVote.vote = vote;
|
||||
const title = this.translate.instant('Are you sure?');
|
||||
const content = this.translate.instant('Your decision cannot be changed afterwards');
|
||||
this.promptService.open(title, content).then(confirmed => {
|
||||
if (confirmed) {
|
||||
this.pollRepo.vote(vote, this.poll.id).catch(this.raiseError);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<mat-card class="motion-poll-wrapper" *ngIf="poll">
|
||||
<mat-card class="motion-poll-wrapper" *ngIf="showPoll">
|
||||
<!-- Poll Infos -->
|
||||
<div class="poll-title-wrapper">
|
||||
<!-- Title Area -->
|
||||
@ -56,7 +56,7 @@
|
||||
|
||||
<!-- Detail link -->
|
||||
<div class="poll-detail-button-wrapper">
|
||||
<a mat-icon-button [routerLink]="pollLink" matTooltip="{{ 'More' | translate }}">
|
||||
<a mat-icon-button [routerLink]="pollLink" matTooltip="{{ 'More' | translate }}" *ngIf="poll.isPublished">
|
||||
<mat-icon class="small-icon">
|
||||
visibility
|
||||
</mat-icon>
|
||||
|
@ -1,4 +1,5 @@
|
||||
@import '~assets/styles/poll-colors.scss';
|
||||
@import '~assets/styles/poll-styles-common.scss';
|
||||
|
||||
.poll-link-wrapper {
|
||||
outline: none;
|
||||
@ -46,18 +47,6 @@
|
||||
div + div {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.yes {
|
||||
color: $votes-yes-color;
|
||||
}
|
||||
|
||||
.no {
|
||||
color: $votes-no-color;
|
||||
}
|
||||
|
||||
.abstain {
|
||||
color: $votes-abstain-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -75,18 +64,6 @@
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.start-poll-button {
|
||||
color: green !important;
|
||||
}
|
||||
|
||||
.stop-poll-button {
|
||||
color: $poll-stop-color;
|
||||
}
|
||||
|
||||
.publish-poll-button {
|
||||
color: $poll-publish-color;
|
||||
}
|
||||
|
||||
.motion-couting-in-progress-hint {
|
||||
margin-top: 1em;
|
||||
font-style: italic;
|
||||
|
@ -7,7 +7,6 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { VotingPrivacyWarningComponent } from 'app/shared/components/voting-privacy-warning/voting-privacy-warning.component';
|
||||
import { PollType } from 'app/shared/models/poll/base-poll';
|
||||
import { infoDialogSettings } from 'app/shared/utils/dialog-settings';
|
||||
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||
import { MotionPollDialogService } from 'app/site/motions/services/motion-poll-dialog.service';
|
||||
@ -15,6 +14,7 @@ import { MotionPollPdfService } from 'app/site/motions/services/motion-poll-pdf.
|
||||
import { MotionPollService } from 'app/site/motions/services/motion-poll.service';
|
||||
import { BasePollComponent } from 'app/site/polls/components/base-poll.component';
|
||||
import { PollService, PollTableData } from 'app/site/polls/services/poll.service';
|
||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||
|
||||
/**
|
||||
* Component to show a motion-poll.
|
||||
@ -48,16 +48,25 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
|
||||
return this.motionPollService.showChart(this.poll);
|
||||
}
|
||||
|
||||
public get hideChangeState(): boolean {
|
||||
return this.poll.isPublished || (this.poll.isCreated && this.poll.type === PollType.Analog);
|
||||
}
|
||||
|
||||
public get reducedPollTableData(): PollTableData[] {
|
||||
return this.motionPollService
|
||||
.generateTableData(this.poll)
|
||||
.filter(data => ['yes', 'no', 'abstain', 'votesinvalid'].includes(data.votingOption));
|
||||
}
|
||||
|
||||
public get showPoll(): boolean {
|
||||
if (this.poll) {
|
||||
if (
|
||||
this.operator.hasPerms('motions.can_manage_polls') ||
|
||||
this.poll.isPublished ||
|
||||
(this.poll.isEVoting && !this.poll.isCreated)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
@ -77,7 +86,8 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
|
||||
pollDialog: MotionPollDialogService,
|
||||
public pollService: PollService,
|
||||
private pdfService: MotionPollPdfService,
|
||||
private motionPollService: MotionPollService
|
||||
private motionPollService: MotionPollService,
|
||||
private operator: OperatorService
|
||||
) {
|
||||
super(titleService, matSnackBar, translate, dialog, promptService, pollRepo, pollDialog);
|
||||
}
|
||||
|
@ -42,15 +42,15 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
|
||||
*/
|
||||
public voteOptionStyle = {
|
||||
Y: {
|
||||
css: 'voted-yes',
|
||||
css: 'yes',
|
||||
icon: 'thumb_up'
|
||||
},
|
||||
N: {
|
||||
css: 'voted-no',
|
||||
css: 'no',
|
||||
icon: 'thumb_down'
|
||||
},
|
||||
A: {
|
||||
css: 'voted-abstain',
|
||||
css: 'abstain',
|
||||
icon: 'trip_origin'
|
||||
}
|
||||
};
|
||||
@ -151,8 +151,6 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
|
||||
this.pollDialog.openDialog(viewPoll);
|
||||
}
|
||||
|
||||
protected onDeleted(): void {}
|
||||
|
||||
/**
|
||||
* Called after the poll has been loaded. Meant to be overwritten by subclasses who need initial access to the poll
|
||||
*/
|
||||
@ -166,6 +164,8 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
|
||||
|
||||
protected abstract hasPerms(): boolean;
|
||||
|
||||
protected abstract onDeleted(): void;
|
||||
|
||||
protected get canSeeVotes(): boolean {
|
||||
return (this.hasPerms && this.poll.isFinished) || this.poll.isPublished;
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ 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 { PollState, PollType } 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';
|
||||
@ -33,6 +33,10 @@ export abstract class BasePollComponent<V extends ViewBasePoll> extends BaseView
|
||||
}
|
||||
};
|
||||
|
||||
public get hideChangeState(): boolean {
|
||||
return this._poll.isPublished || (this._poll.isCreated && this._poll.type === PollType.Analog);
|
||||
}
|
||||
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
matSnackBar: MatSnackBar,
|
||||
|
@ -234,23 +234,27 @@ export abstract class PollService {
|
||||
);
|
||||
}
|
||||
|
||||
public showPercentOfValidOrCast(poll: PollData | ViewBasePoll): boolean {
|
||||
return poll.onehundred_percent_base === PercentBase.Valid || poll.onehundred_percent_base === PercentBase.Cast;
|
||||
}
|
||||
|
||||
public getSumTableKeys(poll: PollData | ViewBasePoll): VotingResult[] {
|
||||
return [
|
||||
{
|
||||
vote: 'votesvalid',
|
||||
hide: poll.votesvalid === -2,
|
||||
showPercent:
|
||||
poll.onehundred_percent_base === PercentBase.Valid ||
|
||||
poll.onehundred_percent_base === PercentBase.Cast
|
||||
showPercent: this.showPercentOfValidOrCast(poll)
|
||||
},
|
||||
{
|
||||
vote: 'votesinvalid',
|
||||
icon: 'not_interested',
|
||||
// TODO || PollType === analog
|
||||
hide: poll.votesinvalid === -2,
|
||||
showPercent: poll.onehundred_percent_base === PercentBase.Cast
|
||||
},
|
||||
{
|
||||
vote: 'votescast',
|
||||
// TODO || PollType === analog
|
||||
hide: poll.votescast === -2,
|
||||
showPercent: poll.onehundred_percent_base === PercentBase.Cast
|
||||
}
|
||||
|
@ -44,8 +44,7 @@ export const allSlides: SlideManifest[] = [
|
||||
{
|
||||
slide: 'motions/motion-poll',
|
||||
path: 'motions/motion-poll',
|
||||
loadChildren: () =>
|
||||
import('./motions/motion-poll/motion-poll-slide.module').then(m => m.MotionPollSlideModule),
|
||||
loadChildren: () => import('./motions/motion-poll/motion-poll-slide.module').then(m => m.MotionPollSlideModule),
|
||||
verboseName: 'Motion Poll',
|
||||
elementIdentifiers: ['name', 'id'],
|
||||
canBeMappedToModel: true
|
||||
@ -137,7 +136,8 @@ export const allSlides: SlideManifest[] = [
|
||||
{
|
||||
slide: 'assignments/assignment-poll',
|
||||
path: 'assignments/assignment-poll',
|
||||
loadChildren: () => import('./assignments/assignment-poll/assignment-poll-slide.module').then(m => m.AssignmentPollSlideModule),
|
||||
loadChildren: () =>
|
||||
import('./assignments/assignment-poll/assignment-poll-slide.module').then(m => m.AssignmentPollSlideModule),
|
||||
verboseName: 'Assignment Poll',
|
||||
elementIdentifiers: ['name', 'id'],
|
||||
canBeMappedToModel: true
|
||||
|
@ -5,6 +5,6 @@ $votes-yes-color: #4caf50;
|
||||
$votes-no-color: #cc6c5b;
|
||||
$votes-abstain-color: #a6a6a6;
|
||||
$vote-active-color: white;
|
||||
$poll-create-color: #4caf50;
|
||||
$poll-start-color: #4caf50;
|
||||
$poll-stop-color: #ff5252;
|
||||
$poll-publish-color: #e6b100;
|
||||
|
40
client/src/assets/styles/poll-styles-common.scss
Normal file
40
client/src/assets/styles/poll-styles-common.scss
Normal file
@ -0,0 +1,40 @@
|
||||
@import '~assets/styles/poll-colors.scss';
|
||||
|
||||
.yes {
|
||||
color: $votes-yes-color;
|
||||
}
|
||||
|
||||
.no {
|
||||
color: $votes-no-color;
|
||||
}
|
||||
|
||||
.abstain {
|
||||
color: $votes-abstain-color;
|
||||
}
|
||||
|
||||
.voted-yes {
|
||||
background-color: $votes-yes-color;
|
||||
color: $vote-active-color;
|
||||
}
|
||||
|
||||
.voted-no {
|
||||
background-color: $votes-no-color;
|
||||
color: $vote-active-color;
|
||||
}
|
||||
|
||||
.voted-abstain {
|
||||
background-color: $votes-abstain-color;
|
||||
color: $vote-active-color;
|
||||
}
|
||||
|
||||
.start-poll-button {
|
||||
color: $poll-start-color;
|
||||
}
|
||||
|
||||
.stop-poll-button {
|
||||
color: $poll-stop-color;
|
||||
}
|
||||
|
||||
.publish-poll-button {
|
||||
color: $poll-publish-color;
|
||||
}
|
@ -65,12 +65,22 @@ def get_config_variables():
|
||||
subgroup="Ballot",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name="assignment_poll_sort_poll_result_by_votes",
|
||||
default_value=True,
|
||||
input_type="boolean",
|
||||
label="Sort election results by amount of votes",
|
||||
weight=420,
|
||||
group="Elections",
|
||||
subgroup="Ballot",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name="assignment_poll_add_candidates_to_list_of_speakers",
|
||||
default_value=True,
|
||||
input_type="boolean",
|
||||
label="Put all candidates on the list of speakers",
|
||||
weight=420,
|
||||
weight=425,
|
||||
group="Elections",
|
||||
subgroup="Ballot",
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user