Enhance Assignment Voting
- repaired the PDF Service for ballots - fixed some permission errors - analog voting has no "started" option anymore - more-link as button - named voting has a progress bar - Shows the poll type for eVoting - Moves and declutters meta info - Enhance the grid and the layout in detail view - declutter and enhance the dot-menus - some other layout changes - remove breadcrumbs in assignment detail - other cleanups refinements - Voting in Assignment over instead of forms (requires more server changes)
This commit is contained in:
parent
524a97cdcc
commit
6044c63c28
@ -116,6 +116,7 @@ export class AssignmentPollRepositoryService extends BasePollRepositoryService<
|
|||||||
return this.translate.instant(plural ? 'Polls' : 'Poll');
|
return this.translate.instant(plural ? 'Polls' : 'Poll');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: data must not be any
|
||||||
public vote(data: any, poll_id: number): Promise<void> {
|
public vote(data: any, poll_id: number): Promise<void> {
|
||||||
return this.http.post(`/rest/assignments/assignment-poll/${poll_id}/vote/`, data);
|
return this.http.post(`/rest/assignments/assignment-poll/${poll_id}/vote/`, data);
|
||||||
}
|
}
|
||||||
|
@ -51,19 +51,19 @@ 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;
|
||||||
|
|
||||||
public get isStateCreated(): boolean {
|
public get isCreated(): boolean {
|
||||||
return this.state === PollState.Created;
|
return this.state === PollState.Created;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isStateStarted(): boolean {
|
public get isStarted(): boolean {
|
||||||
return this.state === PollState.Started;
|
return this.state === PollState.Started;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isStateFinished(): boolean {
|
public get isFinished(): boolean {
|
||||||
return this.state === PollState.Finished;
|
return this.state === PollState.Finished;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isStatePublished(): boolean {
|
public get isPublished(): boolean {
|
||||||
return this.state === PollState.Published;
|
return this.state === PollState.Published;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,20 +71,6 @@ export abstract class BasePoll<T = any, O extends BaseOption<any> = any> extends
|
|||||||
return this.onehundred_percent_base === PercentBase.Valid || this.onehundred_percent_base === PercentBase.Cast;
|
return this.onehundred_percent_base === PercentBase.Valid || this.onehundred_percent_base === PercentBase.Cast;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* If the state is finished.
|
|
||||||
*/
|
|
||||||
public get isFinished(): boolean {
|
|
||||||
return this.state === PollState.Finished;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If the state is published.
|
|
||||||
*/
|
|
||||||
public get isPublished(): boolean {
|
|
||||||
return this.state === PollState.Published;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the state is finished or published
|
* Determine if the state is finished or published
|
||||||
*/
|
*/
|
||||||
|
@ -6,7 +6,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|||||||
// MaterialUI modules
|
// MaterialUI modules
|
||||||
import { MatBadgeModule } from '@angular/material/badge';
|
import { MatBadgeModule } from '@angular/material/badge';
|
||||||
import { MatBottomSheetModule } from '@angular/material/bottom-sheet';
|
import { MatBottomSheetModule } from '@angular/material/bottom-sheet';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule, MatAnchor } from '@angular/material/button';
|
||||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
|
@ -1,16 +1,9 @@
|
|||||||
<os-head-bar
|
<os-head-bar [goBack]="true" [nav]="false">
|
||||||
[goBack]="true"
|
|
||||||
[nav]="false"
|
|
||||||
[hasMainButton]="poll ? poll.type === 'analog' && (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">{{ poll.title }}</h2>
|
<h2 *ngIf="!!poll">{{ poll.title }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="menu-slot" *osPerms="'agenda.can_manage'; or: 'agenda.can_see_list_of_speakers'">
|
<div class="menu-slot" *osPerms="'assignments.can_manage_polls'">
|
||||||
<button type="button" mat-icon-button [matMenuTriggerFor]="pollDetailMenu">
|
<button type="button" mat-icon-button [matMenuTriggerFor]="pollDetailMenu">
|
||||||
<mat-icon>more_vert</mat-icon>
|
<mat-icon>more_vert</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
@ -25,30 +18,14 @@
|
|||||||
<ng-template #viewTemplate>
|
<ng-template #viewTemplate>
|
||||||
<ng-container *ngIf="isReady">
|
<ng-container *ngIf="isReady">
|
||||||
<h1>{{ poll.title }}</h1>
|
<h1>{{ poll.title }}</h1>
|
||||||
<mat-divider></mat-divider>
|
<span *ngIf="poll.type !== 'analog'">{{ poll.typeVerbose | translate }}</span>
|
||||||
<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'">
|
|
||||||
{{ 'Groups' | translate }}:
|
|
||||||
<span *ngFor="let group of poll.groups">{{ group.getTitle() | translate }}</span>
|
|
||||||
</div>
|
|
||||||
<div>{{ 'Voting type' | translate }}: {{ poll.typeVerbose | translate }}</div>
|
|
||||||
<div>{{ 'Election method' | translate }}: {{ poll.pollmethodVerbose | translate }}</div>
|
|
||||||
<div>{{ 'Required majority' | translate }}: {{ poll.majorityMethodVerbose | translate }}</div>
|
|
||||||
<div>{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- TODO Enum -->
|
|
||||||
<div *ngIf="poll.state === 2">
|
|
||||||
<os-poll-progress [poll]="poll"></os-poll-progress>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div *ngIf="poll.stateHasVotes">
|
<div *ngIf="poll.stateHasVotes">
|
||||||
<h2 translate>Result</h2>
|
<h2 translate>Result</h2>
|
||||||
|
|
||||||
<div class="chart-wrapper" [ngClass]="{ flex: isVotedPoll }">
|
<div class="result-wrapper">
|
||||||
<mat-table [dataSource]="poll.tableData">
|
<!-- Result Table -->
|
||||||
|
<mat-table class="result-table" [dataSource]="poll.tableData">
|
||||||
<ng-container matColumnDef="user" sticky>
|
<ng-container matColumnDef="user" sticky>
|
||||||
<mat-header-cell *matHeaderCellDef>{{ 'Candidates' | translate }}</mat-header-cell>
|
<mat-header-cell *matHeaderCellDef>{{ 'Candidates' | translate }}</mat-header-cell>
|
||||||
<mat-cell *matCellDef="let row">{{ row.user }}</mat-cell>
|
<mat-cell *matCellDef="let row">{{ row.user }}</mat-cell>
|
||||||
@ -81,43 +58,64 @@
|
|||||||
<mat-row *matRowDef="let row; columns: columnDefinitionOverview"></mat-row>
|
<mat-row *matRowDef="let row; columns: columnDefinitionOverview"></mat-row>
|
||||||
</mat-table>
|
</mat-table>
|
||||||
|
|
||||||
<div class="chart-inner-wrapper">
|
<!-- Result Chart -->
|
||||||
<os-charts
|
<os-charts
|
||||||
*ngIf="chartDataSubject.value"
|
class="result-chart"
|
||||||
[type]="chartType"
|
*ngIf="chartDataSubject.value"
|
||||||
[labels]="candidatesLabels"
|
[type]="chartType"
|
||||||
[size]="isVotedPoll ? 70 : 100"
|
[labels]="candidatesLabels"
|
||||||
[legendPosition]="isVotedPoll ? 'right' : 'top'"
|
[data]="chartDataSubject"
|
||||||
[showLegend]="true"
|
[hasPadding]="false"
|
||||||
[data]="chartDataSubject"
|
[legendPosition]="isVotedPoll ? 'right' : 'top'"
|
||||||
></os-charts>
|
></os-charts>
|
||||||
|
|
||||||
|
<!-- Named Result -->
|
||||||
|
<div class="named-result-table" *ngIf="poll.type === 'named' && votesDataSource.data">
|
||||||
|
<h3>{{ 'Single votes' | translate }}</h3>
|
||||||
|
<mat-form-field>
|
||||||
|
<input matInput [(ngModel)]="votesDataSource.filter" placeholder="Filter" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-table [dataSource]="votesDataSource">
|
||||||
|
<ng-container matColumnDef="users" sticky>
|
||||||
|
<mat-header-cell *matHeaderCellDef>{{ 'User' | translate }}</mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let row">
|
||||||
|
<div *ngIf="row.user">{{ row.user.getFullName() }}</div>
|
||||||
|
<div *ngIf="!row.user">{{ 'Unknown user' | translate }}</div>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container
|
||||||
|
[matColumnDef]="'votes-' + option.user_id"
|
||||||
|
*ngFor="let option of poll.options"
|
||||||
|
sticky
|
||||||
|
>
|
||||||
|
<mat-header-cell *matHeaderCellDef>
|
||||||
|
<div *ngIf="option.user">{{ option.user.getFullName() }}</div>
|
||||||
|
<div *ngIf="!option.user">{{ 'Unknown user' | translate }}</div>
|
||||||
|
</mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let row">
|
||||||
|
{{ row.votes[option.user_id] }}
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<mat-header-row *matHeaderRowDef="columnDefinitionPerName"></mat-header-row>
|
||||||
|
<mat-row *matRowDef="let row; columns: columnDefinitionPerName"></mat-row>
|
||||||
|
</mat-table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="poll.type === 'named' && votesDataSource.data">
|
<!-- Meta Infos -->
|
||||||
<input matInput [(ngModel)]="votesDataSource.filter" placeholder="Filter" />
|
<div class="poll-content small">
|
||||||
<mat-table [dataSource]="votesDataSource">
|
<div *ngIf="poll.groups && poll.type && poll.type !== 'analog'">
|
||||||
<ng-container matColumnDef="users" sticky>
|
{{ 'Groups' | translate }}:
|
||||||
<mat-header-cell *matHeaderCellDef>{{ 'User' | translate }}</mat-header-cell>
|
|
||||||
<mat-cell *matCellDef="let row">
|
|
||||||
<div *ngIf="row.user">{{ row.user.getFullName() }}</div>
|
|
||||||
<div *ngIf="!row.user">{{ 'Unknown user' | translate }}</div>
|
|
||||||
</mat-cell>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container [matColumnDef]="'votes-' + option.user_id" *ngFor="let option of poll.options" sticky>
|
|
||||||
<mat-header-cell *matHeaderCellDef>
|
|
||||||
<div *ngIf="option.user">{{ option.user.getFullName() }}</div>
|
|
||||||
<div *ngIf="!option.user">{{ 'Unknown user' | translate }}</div>
|
|
||||||
</mat-header-cell>
|
|
||||||
<mat-cell *matCellDef="let row">
|
|
||||||
{{ row.votes[option.user_id] }}
|
|
||||||
</mat-cell>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<mat-header-row *matHeaderRowDef="columnDefinitionPerName"></mat-header-row>
|
<span *ngFor="let group of poll.groups; let i = index">
|
||||||
<mat-row *matRowDef="let row; columns: columnDefinitionPerName"></mat-row>
|
{{ group.getTitle() | translate }}<span *ngIf="i < poll.groups.length - 1">, </span>
|
||||||
</mat-table>
|
</span>
|
||||||
</ng-container>
|
</div>
|
||||||
|
|
||||||
|
<div>{{ 'Required majority' | translate }}: {{ poll.majorityMethodVerbose | translate }}</div>
|
||||||
|
<div>{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@ -125,13 +123,21 @@
|
|||||||
<!-- 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 *ngIf="poll && poll.type === 'named'" (click)="pseudoanonymizePoll()">
|
<button *osPerms="'assignments.can_manage_polls'" mat-menu-item (click)="openDialog()">
|
||||||
<mat-icon>polymer</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
<span translate>Pseudoanonymize</span>
|
<span translate>Edit</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
mat-menu-item
|
||||||
|
*osPerms="'assignments.can_manage_polls'; and: poll && poll.type === 'named'"
|
||||||
|
(click)="pseudoanonymizePoll()"
|
||||||
|
>
|
||||||
|
<mat-icon>warning</mat-icon>
|
||||||
|
<span translate>Anonymize votes</span>
|
||||||
</button>
|
</button>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<button mat-menu-item (click)="deletePoll()">
|
<button *osPerms="'assignments.can_manage_polls'" 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,15 +1,40 @@
|
|||||||
.chart-wrapper {
|
@import '~assets/styles/variables.scss';
|
||||||
&.flex {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.mat-table {
|
.result-wrapper {
|
||||||
flex: 2;
|
display: grid;
|
||||||
.mat-column-votes {
|
grid-gap: 10px;
|
||||||
justify-content: center;
|
grid-template-areas:
|
||||||
}
|
'chart'
|
||||||
}
|
'results'
|
||||||
.chart-inner-wrapper {
|
'names';
|
||||||
flex: 3;
|
}
|
||||||
}
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.named-result-table {
|
||||||
|
grid-area: names;
|
||||||
|
.mat-form-field {
|
||||||
|
font-size: 14px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-content {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
@ -91,6 +91,8 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
|||||||
if (this.isVotedPoll) {
|
if (this.isVotedPoll) {
|
||||||
this._chartType = 'doughnut';
|
this._chartType = 'doughnut';
|
||||||
this.chartDataSubject.next(this.poll.generateCircleChartData());
|
this.chartDataSubject.next(this.poll.generateCircleChartData());
|
||||||
|
} else {
|
||||||
|
super.initChartData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,79 +1,80 @@
|
|||||||
<ng-container *ngIf="poll">
|
<ng-container *ngIf="poll">
|
||||||
|
<!-- Poll progress bar -->
|
||||||
|
<div *osPerms="'assignments.can_manage_polls'; and: poll.isStarted">
|
||||||
|
<os-poll-progress [poll]="poll"></os-poll-progress>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="vmanager.canVote(poll)">
|
<ng-container *ngIf="vmanager.canVote(poll)">
|
||||||
<form *ngIf="voteForm" [formGroup]="voteForm" class="voting-grid">
|
<!-- Leftover votes -->
|
||||||
<!-- empty divs to fit the grid -->
|
<h4 *ngIf="poll.pollmethod === pollMethods.Votes">
|
||||||
<div></div><div></div>
|
{{ 'Votes for this poll' | translate }}: {{ poll.votes_amount }}
|
||||||
<div>
|
<!-- ({{ getVotesCount() }}/{{ poll.votes_amount }} {{ 'Votes' | translate }}) -->
|
||||||
<span *ngIf="poll.pollmethod === pollMethods.Votes">
|
</h4>
|
||||||
({{ getVotesCount() }}/{{ poll.votes_amount }} {{ 'Votes' | translate }})
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- candidate votes -->
|
<!-- Options and Actions -->
|
||||||
<ng-container *ngFor="let option of poll.options" formGroupName="votes">
|
<div *ngFor="let option of poll.options; let i = index">
|
||||||
<div>
|
<div
|
||||||
<span *ngIf="option.user">{{ option.user.getFullName() }}</span>
|
[ngClass]="{
|
||||||
<span *ngIf="!option.user">{{ "Unknown user" | translate }}</span>
|
'yna-grid': poll.pollmethod === pollMethods.YNA,
|
||||||
</div>
|
'yn-grid': poll.pollmethod === pollMethods.YN,
|
||||||
|
'single-vote-grid': poll.pollmethod === pollMethods.Votes
|
||||||
<div class="current-vote">
|
}"
|
||||||
<ng-container *ngIf="poll.pollmethod !== pollMethods.Votes && currentVotes[option.user_id]">
|
|
||||||
({{ 'Current' | translate }}: {{ currentVotes[option.user_id] | translate }})
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="poll.pollmethod === pollMethods.Votes && currentVotes[option.user_id]">
|
|
||||||
({{ 'Current choice' | translate }})
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<mat-radio-group
|
|
||||||
name="votes-{{ poll.id }}-{{ option.id }}"
|
|
||||||
[formControlName]="option.id"
|
|
||||||
>
|
|
||||||
<mat-radio-button value="Y" (click)="yesButtonClicked($event, option.id.toString())">
|
|
||||||
<span translate>Yes</span>
|
|
||||||
</mat-radio-button>
|
|
||||||
<mat-radio-button value="N" *ngIf="poll.pollmethod !== pollMethods.Votes">
|
|
||||||
<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>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- global no/abstain -->
|
|
||||||
<ng-container *ngIf="poll.pollmethod === pollMethods.Votes && (poll.global_no || poll.global_abstain)">
|
|
||||||
<!-- empty div to fit the grid -->
|
|
||||||
<div></div>
|
|
||||||
<div class="current-vote">
|
|
||||||
<ng-container *ngIf="currentVotes.global">
|
|
||||||
({{ 'Current' | translate }}: {{ currentVotes.global | translate }})
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
<mat-radio-group
|
|
||||||
name="votes-{{ poll.id }}-global"
|
|
||||||
formControlName="global"
|
|
||||||
>
|
|
||||||
<mat-radio-button value="N" *ngIf="poll.global_no">
|
|
||||||
<span translate>Global no</span>
|
|
||||||
</mat-radio-button>
|
|
||||||
<mat-radio-button value="A" *ngIf="poll.global_abstain">
|
|
||||||
<span translate>Global abstain</span>
|
|
||||||
</mat-radio-button>
|
|
||||||
</mat-radio-group>
|
|
||||||
</ng-container>
|
|
||||||
</form>
|
|
||||||
<div class="right-align" *ngIf="poll.type !== PollType.Named || poll.pollmethod !== pollMethods.Votes">
|
|
||||||
<button
|
|
||||||
mat-button
|
|
||||||
mat-button-default
|
|
||||||
(click)="saveVotes()"
|
|
||||||
[disabled]="isSaveButtonDisabled()"
|
|
||||||
>
|
>
|
||||||
<span translate>Save</span>
|
<div class="vote-candidate-name">
|
||||||
</button>
|
<span *ngIf="option.user">{{ option.user.getFullName() }}</span>
|
||||||
|
<span *ngIf="!option.user">{{ 'Unknown user' | translate }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngFor="let action of voteActions">
|
||||||
|
<button
|
||||||
|
mat-raised-button
|
||||||
|
(click)="saveSingleVote(option.id, action.vote)"
|
||||||
|
[ngClass]="currentVotes[option.id] ? action.css : ''"
|
||||||
|
>
|
||||||
|
<mat-icon> {{ action.icon }}</mat-icon>
|
||||||
|
</button>
|
||||||
|
<span *ngIf="poll.pollmethod !== pollMethods.Votes" class="vote-label">
|
||||||
|
{{ action.label | translate }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<mat-divider *ngIf="poll.options.length - 1 > i"></mat-divider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- global no/abstain -->
|
||||||
|
<ng-container *ngIf="poll.pollmethod === pollMethods.Votes && (poll.global_no || poll.global_abstain)">
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<div class="global-option-grid">
|
||||||
|
<div *ngIf="poll.global_no">
|
||||||
|
<button
|
||||||
|
mat-raised-button
|
||||||
|
(click)="saveGlobalVote('N')"
|
||||||
|
[ngClass]="currentVotes['global'] === 'No' ? 'voted-no' : ''"
|
||||||
|
>
|
||||||
|
<mat-icon> thumb_down </mat-icon>
|
||||||
|
</button>
|
||||||
|
<span class="vote-label">
|
||||||
|
{{ 'No to all' | translate }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="poll.global_abstain">
|
||||||
|
<button
|
||||||
|
mat-raised-button
|
||||||
|
(click)="saveGlobalVote('A')"
|
||||||
|
[ngClass]="currentVotes['global'] === 'Abstain' ? 'voted-abstain' : ''"
|
||||||
|
>
|
||||||
|
<mat-icon> trip_origin</mat-icon>
|
||||||
|
</button>
|
||||||
|
<span class="vote-label">
|
||||||
|
{{ 'Abstain' | translate }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Shows the permission error -->
|
||||||
<ng-container *ngIf="!vmanager.canVote(poll)">
|
<ng-container *ngIf="!vmanager.canVote(poll)">
|
||||||
<span>{{ vmanager.getVotePermissionErrorVerbose(poll) | translate }}</span>
|
<span>{{ vmanager.getVotePermissionErrorVerbose(poll) | translate }}</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -1,12 +1,61 @@
|
|||||||
.current-vote {
|
@import '~assets/styles/poll-colors.scss';
|
||||||
color: #777;
|
|
||||||
margin-right: 10px;
|
%vote-grid-base {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 10px;
|
||||||
|
margin: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.voting-grid {
|
.yn-grid {
|
||||||
display: grid;
|
@extend %vote-grid-base;
|
||||||
grid-gap: 5px;
|
grid-template-areas:
|
||||||
padding: 5px;
|
'name name'
|
||||||
align-items: baseline;
|
'yes no';
|
||||||
grid-template-columns: auto max-content max-content;
|
}
|
||||||
|
|
||||||
|
.yna-grid {
|
||||||
|
@extend %vote-grid-base;
|
||||||
|
grid-template-areas:
|
||||||
|
'name name name'
|
||||||
|
'yes no abstain';
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-vote-grid {
|
||||||
|
@extend %vote-grid-base;
|
||||||
|
grid-template-areas: 'yes name';
|
||||||
|
grid-template-columns: min-content auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-option-grid {
|
||||||
|
@extend %vote-grid-base;
|
||||||
|
grid-template-columns: auto auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-candidate-name {
|
||||||
|
grid-area: name;
|
||||||
|
display: flex;
|
||||||
|
span {
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.voted-yes {
|
||||||
|
background-color: $votes-yes-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voted-no {
|
||||||
|
background-color: $votes-no-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voted-abstain {
|
||||||
|
background-color: $votes-abstain-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-label {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-divider-horizontal {
|
||||||
|
position: initial;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
|
|
||||||
import { MatSnackBar } from '@angular/material';
|
import { MatSnackBar } from '@angular/material';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
@ -15,6 +14,14 @@ import { BasePollVoteComponent } from 'app/site/polls/components/base-poll-vote.
|
|||||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||||
import { ViewAssignmentVote } from '../../models/view-assignment-vote';
|
import { ViewAssignmentVote } from '../../models/view-assignment-vote';
|
||||||
|
|
||||||
|
// TODO: Duplicate
|
||||||
|
interface VoteActions {
|
||||||
|
vote: 'Y' | 'N' | 'A';
|
||||||
|
css: string;
|
||||||
|
icon: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'os-assignment-poll-vote',
|
selector: 'os-assignment-poll-vote',
|
||||||
templateUrl: './assignment-poll-vote.component.html',
|
templateUrl: './assignment-poll-vote.component.html',
|
||||||
@ -23,8 +30,7 @@ import { ViewAssignmentVote } from '../../models/view-assignment-vote';
|
|||||||
export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssignmentPoll> implements OnInit {
|
export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssignmentPoll> implements OnInit {
|
||||||
public pollMethods = AssignmentPollMethods;
|
public pollMethods = AssignmentPollMethods;
|
||||||
public PollType = PollType;
|
public PollType = PollType;
|
||||||
|
public voteActions: VoteActions[] = [];
|
||||||
public voteForm: FormGroup;
|
|
||||||
|
|
||||||
/** holds the currently saved votes */
|
/** holds the currently saved votes */
|
||||||
public currentVotes: { [key: number]: string | null; global?: string } = {};
|
public currentVotes: { [key: number]: string | null; global?: string } = {};
|
||||||
@ -38,13 +44,13 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
|
|||||||
vmanager: VotingService,
|
vmanager: VotingService,
|
||||||
operator: OperatorService,
|
operator: OperatorService,
|
||||||
private voteRepo: AssignmentVoteRepositoryService,
|
private voteRepo: AssignmentVoteRepositoryService,
|
||||||
private pollRepo: AssignmentPollRepositoryService,
|
private pollRepo: AssignmentPollRepositoryService
|
||||||
private formBuilder: FormBuilder
|
|
||||||
) {
|
) {
|
||||||
super(title, translate, matSnackbar, vmanager, operator);
|
super(title, translate, matSnackbar, vmanager, operator);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
|
this.defineVoteOptions();
|
||||||
this.subscriptions.push(
|
this.subscriptions.push(
|
||||||
this.voteRepo.getViewModelListObservable().subscribe(votes => {
|
this.voteRepo.getViewModelListObservable().subscribe(votes => {
|
||||||
this.votes = votes;
|
this.votes = votes;
|
||||||
@ -53,119 +59,74 @@ export class AssignmentPollVoteComponent extends BasePollVoteComponent<ViewAssig
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private defineVoteOptions(): void {
|
||||||
|
this.voteActions.push({
|
||||||
|
vote: 'Y',
|
||||||
|
css: 'voted-yes',
|
||||||
|
icon: 'thumb_up',
|
||||||
|
label: 'Yes'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.poll.pollmethod !== AssignmentPollMethods.Votes) {
|
||||||
|
this.voteActions.push({
|
||||||
|
vote: 'N',
|
||||||
|
css: 'voted-no',
|
||||||
|
icon: 'thumb_down',
|
||||||
|
label: 'No'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.poll.pollmethod === AssignmentPollMethods.YNA) {
|
||||||
|
this.voteActions.push({
|
||||||
|
vote: 'A',
|
||||||
|
css: 'voted-abstain',
|
||||||
|
icon: 'trip_origin',
|
||||||
|
label: 'Abstain'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected updateVotes(): void {
|
protected updateVotes(): void {
|
||||||
if (this.user && this.votes && this.poll) {
|
if (this.user && this.votes && this.poll) {
|
||||||
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
|
||||||
);
|
);
|
||||||
this.voteForm = this.formBuilder.group({
|
|
||||||
votes: this.formBuilder.group(
|
|
||||||
this.poll.options.mapToObject(option => ({ [option.id]: ['', [Validators.required]] }))
|
|
||||||
)
|
|
||||||
});
|
|
||||||
if (
|
|
||||||
this.poll.pollmethod === AssignmentPollMethods.Votes &&
|
|
||||||
(this.poll.global_no || this.poll.global_abstain)
|
|
||||||
) {
|
|
||||||
this.voteForm.addControl('global', new FormControl('', Validators.required));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const option of this.poll.options) {
|
for (const option of this.poll.options) {
|
||||||
let curr_vote = filtered.find(vote => vote.option.id === option.id);
|
let curr_vote = filtered.find(vote => vote.option.id === option.id);
|
||||||
if (this.poll.pollmethod === AssignmentPollMethods.Votes && curr_vote) {
|
if (this.poll.pollmethod === AssignmentPollMethods.Votes && curr_vote) {
|
||||||
if (curr_vote.value !== 'Y') {
|
if (curr_vote.value !== 'Y') {
|
||||||
this.currentVotes.global = curr_vote.valueVerbose;
|
this.currentVotes.global = curr_vote.valueVerbose;
|
||||||
this.voteForm.controls.global.setValue(curr_vote.value);
|
|
||||||
curr_vote = null;
|
curr_vote = null;
|
||||||
} else {
|
} else {
|
||||||
this.currentVotes.global = null;
|
this.currentVotes.global = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.currentVotes[option.user_id] = curr_vote && curr_vote.valueVerbose;
|
this.currentVotes[option.id] = curr_vote && curr_vote.valueVerbose;
|
||||||
this.voteForm.get(['votes', option.id]).setValue(curr_vote && curr_vote.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.poll.pollmethod === AssignmentPollMethods.Votes) {
|
|
||||||
this.voteForm.controls.votes.valueChanges.subscribe(value => {
|
|
||||||
if (Object.values(value).some(vote => vote)) {
|
|
||||||
const ctrl = this.voteForm.controls.global;
|
|
||||||
if (ctrl) {
|
|
||||||
ctrl.reset();
|
|
||||||
}
|
|
||||||
this.saveVotesIfNamed();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.voteForm.controls.global.valueChanges.subscribe(value => {
|
|
||||||
if (value) {
|
|
||||||
this.voteForm.controls.votes.reset();
|
|
||||||
this.saveVotesIfNamed();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private saveVotesIfNamed(): void {
|
private getPollOptionIds(): number[] {
|
||||||
if (this.poll.type === PollType.Named && !this.isSaveButtonDisabled()) {
|
return this.poll.options.map(option => option.id);
|
||||||
this.saveVotes();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public saveVotes(): void {
|
public saveSingleVote(optionId: number, vote: 'Y' | 'N' | 'A'): void {
|
||||||
let values = this.voteForm.value.votes;
|
const pollOptionIds = this.getPollOptionIds();
|
||||||
// convert Y to 1 and null to 0 for votes method
|
const requestMap = pollOptionIds.reduce((o, n) => {
|
||||||
if (this.poll.pollmethod === this.pollMethods.Votes) {
|
if ((n === optionId && vote === 'Y') !== (this.currentVotes[n] === 'Yes')) {
|
||||||
if (this.voteForm.value.global) {
|
o[n] = 1;
|
||||||
values = JSON.stringify(this.voteForm.value.global);
|
|
||||||
} else {
|
} else {
|
||||||
this.poll.options.forEach(option => {
|
o[n] = 0;
|
||||||
values[option.id] = this.voteForm.value.votes[option.id] === 'Y' ? 1 : 0;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
this.pollRepo.vote(values, this.poll.id).catch(this.raiseError);
|
return o;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
this.pollRepo.vote(JSON.stringify(requestMap), this.poll.id).catch(this.raiseError);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isSaveButtonDisabled(): boolean {
|
public saveGlobalVote(globalVote: 'N' | 'A'): void {
|
||||||
return (
|
this.pollRepo.vote(`"${globalVote}"`, this.poll.id).catch(this.raiseError);
|
||||||
!this.voteForm ||
|
|
||||||
this.voteForm.pristine ||
|
|
||||||
(this.poll.pollmethod === AssignmentPollMethods.Votes
|
|
||||||
? !this.getAllFormControls().some(control => control.valid)
|
|
||||||
: this.voteForm.invalid)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getVotesCount(): number {
|
|
||||||
return Object.values(this.voteForm.value.votes).filter(vote => vote).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getAllFormControls(): AbstractControl[] {
|
|
||||||
if (this.voteForm) {
|
|
||||||
const votesFormGroup = this.voteForm.controls.votes as FormGroup;
|
|
||||||
return [...Object.values(votesFormGroup.controls), this.voteForm.controls.global];
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public yesButtonClicked($event: MouseEvent, optionId: string): void {
|
|
||||||
if (this.poll.pollmethod === AssignmentPollMethods.Votes) {
|
|
||||||
// check current value (before click)
|
|
||||||
if (this.voteForm.value.votes[optionId] === 'Y') {
|
|
||||||
// this handler is executed before the mat-radio-button handler, so we have to set a timeout or else the other handler will just set the value again
|
|
||||||
setTimeout(() => {
|
|
||||||
this.voteForm.get(['votes', optionId]).setValue(null);
|
|
||||||
this.voteForm.markAsDirty();
|
|
||||||
this.saveVotesIfNamed();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// check if by clicking this button, the amount of votes would succeed the permitted amount
|
|
||||||
if (this.getVotesCount() >= this.poll.votes_amount) {
|
|
||||||
$event.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<mat-card class="os-card" *ngIf="poll">
|
<mat-card class="os-card" *ngIf="poll && showPoll()">
|
||||||
<div class="assignment-poll-wrapper">
|
<div class="assignment-poll-wrapper">
|
||||||
<div class="assignment-poll-title-header">
|
<div class="assignment-poll-title-header">
|
||||||
<mat-card-title>
|
<mat-card-title>
|
||||||
@ -8,28 +8,24 @@
|
|||||||
</mat-card-title>
|
</mat-card-title>
|
||||||
<div class="poll-properties">
|
<div class="poll-properties">
|
||||||
<mat-chip
|
<mat-chip
|
||||||
*osPerms="'assignments.can_manage'"
|
*osPerms="'assignments.can_manage_polls'"
|
||||||
class="poll-state active"
|
class="poll-state active"
|
||||||
[disableRipple]="true"
|
[disableRipple]="true"
|
||||||
[matMenuTriggerFor]="triggerMenu"
|
[matMenuTriggerFor]="triggerMenu"
|
||||||
[ngClass]="poll.stateVerbose.toLowerCase()"
|
[class]="poll.stateVerbose.toLowerCase()"
|
||||||
>
|
[ngClass]="{ disabled: !poll.getNextStates() }"
|
||||||
{{ poll.stateVerbose | translate }}
|
|
||||||
</mat-chip>
|
|
||||||
<mat-chip
|
|
||||||
*ngIf="!canManage && poll.isPublished"
|
|
||||||
[disableRipple]="true"
|
|
||||||
class="poll-state active"
|
|
||||||
[ngClass]="poll.stateVerbose.toLowerCase()"
|
|
||||||
>
|
>
|
||||||
{{ poll.stateVerbose | translate }}
|
{{ poll.stateVerbose | translate }}
|
||||||
</mat-chip>
|
</mat-chip>
|
||||||
|
<span *ngIf="poll.type !== 'analog'">
|
||||||
|
{{ poll.typeVerbose | translate }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="poll-menu">
|
<div class="poll-menu">
|
||||||
<!-- Buttons -->
|
<!-- Buttons -->
|
||||||
<button
|
<button
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
*osPerms="'assignments.can_manage'; "core.can_manage_projector""
|
*osPerms="'assignments.motions.can_manage_polls';or: 'core.can_manage_projector'"
|
||||||
[matMenuTriggerFor]="pollItemMenu"
|
[matMenuTriggerFor]="pollItemMenu"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
>
|
>
|
||||||
@ -39,9 +35,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="hasVotes">
|
<div *ngIf="hasVotes">
|
||||||
<os-charts [type]="chartType" [labels]="candidatesLabels" [data]="chartDataSubject"></os-charts>
|
<os-charts
|
||||||
|
[type]="chartType"
|
||||||
|
[labels]="candidatesLabels"
|
||||||
|
[data]="chartDataSubject"
|
||||||
|
[hasPadding]="false"
|
||||||
|
></os-charts>
|
||||||
</div>
|
</div>
|
||||||
<os-assignment-poll-vote *ngIf="poll.canBeVotedFor" [poll]="poll"></os-assignment-poll-vote>
|
<os-assignment-poll-vote *ngIf="poll.canBeVotedFor" [poll]="poll"></os-assignment-poll-vote>
|
||||||
|
<div class="poll-detail-button-wrapper">
|
||||||
|
<a mat-button routerLink="/assignments/polls/{{ poll.id }}">
|
||||||
|
{{ 'More' | translate }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|
||||||
@ -64,6 +70,10 @@
|
|||||||
<os-projector-button menuItem="true" [object]="poll"></os-projector-button>
|
<os-projector-button menuItem="true" [object]="poll"></os-projector-button>
|
||||||
</div>
|
</div>
|
||||||
<div *osPerms="'assignments.can_manage'">
|
<div *osPerms="'assignments.can_manage'">
|
||||||
|
<button mat-menu-item (click)="printBallot()">
|
||||||
|
<mat-icon>picture_as_pdf</mat-icon>
|
||||||
|
<span translate>Ballot paper</span>
|
||||||
|
</button>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<button mat-menu-item class="red-warning-text" (click)="onDeletePoll()">
|
<button mat-menu-item class="red-warning-text" (click)="onDeletePoll()">
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon>delete</mat-icon>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
.assignment-poll-wrapper {
|
.assignment-poll-wrapper {
|
||||||
@import '~assets/styles/poll-common-styles.scss';
|
@import '~assets/styles/poll-common-styles.scss';
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0 15px;
|
margin: 0 15px;
|
||||||
|
|
||||||
.poll-menu {
|
.poll-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -39,4 +39,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.poll-detail-button-wrapper {
|
||||||
|
display: flex;
|
||||||
|
margin: auto 0;
|
||||||
|
> a {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import { PollState } from 'app/shared/models/poll/base-poll';
|
|||||||
import { BasePollComponent } from 'app/site/polls/components/base-poll.component';
|
import { BasePollComponent } from 'app/site/polls/components/base-poll.component';
|
||||||
import { PollService } from 'app/site/polls/services/poll.service';
|
import { PollService } from 'app/site/polls/services/poll.service';
|
||||||
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
|
||||||
|
import { AssignmentPollPdfService } from '../../services/assignment-poll-pdf.service';
|
||||||
import { ViewAssignmentOption } from '../../models/view-assignment-option';
|
import { ViewAssignmentOption } from '../../models/view-assignment-option';
|
||||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||||
|
|
||||||
@ -89,7 +90,8 @@ export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPol
|
|||||||
pollDialog: AssignmentPollDialogService,
|
pollDialog: AssignmentPollDialogService,
|
||||||
public pollService: PollService,
|
public pollService: PollService,
|
||||||
private operator: OperatorService,
|
private operator: OperatorService,
|
||||||
private formBuilder: FormBuilder
|
private formBuilder: FormBuilder,
|
||||||
|
private pdfService: AssignmentPollPdfService
|
||||||
) {
|
) {
|
||||||
super(titleService, matSnackBar, translate, dialog, promptService, repo, pollDialog);
|
super(titleService, matSnackBar, translate, dialog, promptService, repo, pollDialog);
|
||||||
}
|
}
|
||||||
@ -105,11 +107,17 @@ export class AssignmentPollComponent extends BasePollComponent<ViewAssignmentPol
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Print the PDF of this poll with the corresponding options and numbers
|
* Print the PDF of this poll with the corresponding options and numbers
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
public printBallot(): void {
|
public printBallot(): void {
|
||||||
throw new Error('TODO');
|
this.pdfService.printBallots(this.poll);
|
||||||
// this.pdfService.printBallots(this.poll);
|
}
|
||||||
|
|
||||||
|
public showPoll(): boolean {
|
||||||
|
return (
|
||||||
|
this.operator.hasPerms('assignments.can_manage_polls') ||
|
||||||
|
this.poll.isPublished ||
|
||||||
|
(this.poll.type !== 'analog' && this.poll.isStarted)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2,7 +2,7 @@ import { BehaviorSubject } from 'rxjs';
|
|||||||
|
|
||||||
import { ChartData } from 'app/shared/components/charts/charts.component';
|
import { ChartData } from 'app/shared/components/charts/charts.component';
|
||||||
import { AssignmentPoll, AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
import { AssignmentPoll, AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
||||||
import { PollColor } from 'app/shared/models/poll/base-poll';
|
import { PollColor, PollState } from 'app/shared/models/poll/base-poll';
|
||||||
import { BaseViewModel } from 'app/site/base/base-view-model';
|
import { BaseViewModel } from 'app/site/base/base-view-model';
|
||||||
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
||||||
import { PollData, ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
import { PollData, ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
||||||
@ -87,6 +87,16 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll> implements
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override from base poll to skip started state in analog poll type
|
||||||
|
*/
|
||||||
|
public getNextStates(): { [key: number]: string } {
|
||||||
|
if (this.poll.type === 'analog' && this.state === PollState.Created) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return super.getNextStates();
|
||||||
|
}
|
||||||
|
|
||||||
public getPercentBase(): number {
|
public getPercentBase(): number {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import { PdfDocumentService } from 'app/core/pdf-services/pdf-document.service';
|
|||||||
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
|
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
|
||||||
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
||||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||||
|
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
|
||||||
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
import { ViewAssignmentPoll } from '../models/view-assignment-poll';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -138,7 +139,7 @@ export class AssignmentPollPdfService extends PollPdfService {
|
|||||||
|
|
||||||
// TODO: typing of result
|
// TODO: typing of result
|
||||||
private createCandidateFields(poll: ViewAssignmentPoll): object {
|
private createCandidateFields(poll: ViewAssignmentPoll): object {
|
||||||
/*const candidates = poll.options.sort((a, b) => {
|
const candidates = poll.options.sort((a, b) => {
|
||||||
return a.weight - b.weight;
|
return a.weight - b.weight;
|
||||||
});
|
});
|
||||||
const resultObject = candidates.map(cand => {
|
const resultObject = candidates.map(cand => {
|
||||||
@ -151,13 +152,12 @@ export class AssignmentPollPdfService extends PollPdfService {
|
|||||||
noEntry.margin[1] = 25;
|
noEntry.margin[1] = 25;
|
||||||
resultObject.push(noEntry);
|
resultObject.push(noEntry);
|
||||||
}
|
}
|
||||||
return resultObject;*/
|
return resultObject;
|
||||||
throw new Error('TODO');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: typing of result
|
// TODO: typing of result
|
||||||
/*private createYNBallotEntry(option: string, method: AssignmentPollmethods): object {
|
private createYNBallotEntry(option: string, method: AssignmentPollMethods): object {
|
||||||
const choices = method === 'yna' ? ['Yes', 'No', 'Abstain'] : ['Yes', 'No'];
|
const choices = method === 'YNA' ? ['Yes', 'No', 'Abstain'] : ['Yes', 'No'];
|
||||||
const columnstack = choices.map(choice => {
|
const columnstack = choices.map(choice => {
|
||||||
return {
|
return {
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
@ -174,7 +174,7 @@ export class AssignmentPollPdfService extends PollPdfService {
|
|||||||
columns: columnstack
|
columns: columnstack
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}*/
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the poll description
|
* Generates the poll description
|
||||||
@ -184,10 +184,9 @@ export class AssignmentPollPdfService extends PollPdfService {
|
|||||||
*/
|
*/
|
||||||
// TODO: typing of result
|
// TODO: typing of result
|
||||||
private createPollHint(poll: ViewAssignmentPoll): object {
|
private createPollHint(poll: ViewAssignmentPoll): object {
|
||||||
/*return {
|
return {
|
||||||
text: poll.description || '',
|
text: poll.description || '',
|
||||||
style: 'description'
|
style: 'description'
|
||||||
};*/
|
};
|
||||||
throw new Error('TODO');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,6 @@
|
|||||||
class="result-chart"
|
class="result-chart"
|
||||||
*ngIf="chartDataSubject.value"
|
*ngIf="chartDataSubject.value"
|
||||||
[type]="chartType"
|
[type]="chartType"
|
||||||
[showLegend]="true"
|
|
||||||
[data]="chartDataSubject"
|
[data]="chartDataSubject"
|
||||||
></os-charts>
|
></os-charts>
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Publish immediately button. Only show for new polls -->
|
<!-- Publish immediately button. Only show for new polls -->
|
||||||
<div *ngIf="!pollData.isStatePublished">
|
<div *ngIf="!pollData.isPublished">
|
||||||
<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>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<ng-container *ngIf="poll">
|
<ng-container *ngIf="poll">
|
||||||
<div *osPerms="'motions.can_manage_polls';and:poll.isStateStarted">
|
<div *osPerms="'motions.can_manage_polls';and:poll.isStarted">
|
||||||
<os-poll-progress [poll]="poll"></os-poll-progress>
|
<os-poll-progress [poll]="poll"></os-poll-progress>
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="vmanager.canVote(poll)">
|
<ng-container *ngIf="vmanager.canVote(poll)">
|
||||||
|
@ -20,19 +20,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<mat-chip
|
<mat-chip
|
||||||
*ngIf="poll.getNextStates()"
|
|
||||||
disableRipple
|
disableRipple
|
||||||
class="poll-state active"
|
|
||||||
[matMenuTriggerFor]="triggerMenu"
|
[matMenuTriggerFor]="triggerMenu"
|
||||||
[ngClass]="poll.stateVerbose.toLowerCase()"
|
|
||||||
>
|
|
||||||
{{ poll.stateVerbose }}
|
|
||||||
</mat-chip>
|
|
||||||
<mat-chip
|
|
||||||
*ngIf="!poll.getNextStates()"
|
|
||||||
disableRipple
|
|
||||||
class="poll-state active"
|
class="poll-state active"
|
||||||
[ngClass]="poll.stateVerbose.toLowerCase()"
|
[class]="poll.stateVerbose.toLowerCase()"
|
||||||
|
[ngClass]="{ 'disabled': !poll.getNextStates() }"
|
||||||
>
|
>
|
||||||
{{ poll.stateVerbose }}
|
{{ poll.stateVerbose }}
|
||||||
</mat-chip>
|
</mat-chip>
|
||||||
@ -87,9 +79,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="poll-detail-button-wrapper">
|
<div class="poll-detail-button-wrapper">
|
||||||
<button mat-button [routerLink]="pollLink">
|
<a mat-button [routerLink]="pollLink">
|
||||||
{{ 'More' | translate }}
|
{{ 'More' | translate }}
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@
|
|||||||
.poll-detail-button-wrapper {
|
.poll-detail-button-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: auto 0;
|
margin: auto 0;
|
||||||
> button {
|
> a {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,8 +130,8 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll> {
|
|||||||
public showPoll(): boolean {
|
public showPoll(): boolean {
|
||||||
return (
|
return (
|
||||||
this.operator.hasPerms('motions.can_manage_polls') ||
|
this.operator.hasPerms('motions.can_manage_polls') ||
|
||||||
this.poll.isStatePublished ||
|
this.poll.isPublished ||
|
||||||
(this.poll.type !== 'analog' && this.poll.isStateStarted)
|
(this.poll.type !== 'analog' && this.poll.isStarted)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<form [formGroup]="contentForm" class="poll-preview-meta-info-form">
|
<form [formGroup]="contentForm" class="poll-preview-meta-info-form">
|
||||||
<ng-container *ngIf="!data || !data.state || data.isStateCreated">
|
<ng-container *ngIf="!data || !data.state || data.isCreated">
|
||||||
<!-- Poll Type -->
|
<!-- Poll Type -->
|
||||||
<mat-form-field *ngIf="pollService.isElectronicVotingEnabled">
|
<mat-form-field *ngIf="pollService.isElectronicVotingEnabled">
|
||||||
<mat-select [placeholder]="PollPropertyVerbose.type | translate" formControlName="type" required>
|
<mat-select [placeholder]="PollPropertyVerbose.type | translate" formControlName="type" required>
|
||||||
@ -71,7 +71,7 @@
|
|||||||
|
|
||||||
<!-- Amount of Votes -->
|
<!-- Amount of Votes -->
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngIf="contentForm.get('pollmethod').value === 'votes' && (!data || !data.state || data.isStateCreated)"
|
*ngIf="contentForm.get('pollmethod').value === 'votes' && (!data || !data.state || data.isCreated)"
|
||||||
>
|
>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<input
|
<input
|
||||||
|
@ -856,6 +856,13 @@ button.mat-menu-item.selected {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use to disable events on (i.e) matMenuTriggerFor
|
||||||
|
*/
|
||||||
|
.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
// custom horrizontal scroll-bar
|
// custom horrizontal scroll-bar
|
||||||
|
|
||||||
.h-scroller {
|
.h-scroller {
|
||||||
|
@ -407,7 +407,7 @@ class AssignmentPollViewSet(BasePollViewSet):
|
|||||||
|
|
||||||
YN/YNA:
|
YN/YNA:
|
||||||
{<option_id>: 'Y' | 'N' [|'A']}
|
{<option_id>: 'Y' | 'N' [|'A']}
|
||||||
- all option_ids must be given
|
- all option_ids must be given TODO: No it must not be that way. Single Votes have to be accepted
|
||||||
- 'A' is only allowed in YNA pollmethod
|
- 'A' is only allowed in YNA pollmethod
|
||||||
|
|
||||||
Votes for all options have to be given
|
Votes for all options have to be given
|
||||||
|
Loading…
Reference in New Issue
Block a user