assignment improvements
- layouting - fix one-vote polls - filter candidate list - candidate sorting - fix adding/removing of candidates - avoid ui jumping - fix quorum calculations
This commit is contained in:
parent
0f1df91915
commit
9dfac94099
@ -93,33 +93,40 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito
|
|||||||
private createViewAssignmentRelatedUsers(
|
private createViewAssignmentRelatedUsers(
|
||||||
assignmentRelatedUsers: AssignmentRelatedUser[]
|
assignmentRelatedUsers: AssignmentRelatedUser[]
|
||||||
): ViewAssignmentRelatedUser[] {
|
): ViewAssignmentRelatedUser[] {
|
||||||
return assignmentRelatedUsers.map(aru => {
|
return assignmentRelatedUsers
|
||||||
|
.map(aru => {
|
||||||
const user = this.viewModelStoreService.get(ViewUser, aru.user_id);
|
const user = this.viewModelStoreService.get(ViewUser, aru.user_id);
|
||||||
return new ViewAssignmentRelatedUser(aru, user);
|
return new ViewAssignmentRelatedUser(aru, user);
|
||||||
});
|
})
|
||||||
|
.sort((a, b) => a.weight - b.weight);
|
||||||
}
|
}
|
||||||
|
|
||||||
private createViewAssignmentPolls(assignmentPolls: AssignmentPoll[]): ViewAssignmentPoll[] {
|
private createViewAssignmentPolls(assignmentPolls: AssignmentPoll[]): ViewAssignmentPoll[] {
|
||||||
return assignmentPolls.map(poll => {
|
return assignmentPolls.map(poll => {
|
||||||
const options = poll.options.map(option => {
|
const options = poll.options
|
||||||
|
.map(option => {
|
||||||
const user = this.viewModelStoreService.get(ViewUser, option.candidate_id);
|
const user = this.viewModelStoreService.get(ViewUser, option.candidate_id);
|
||||||
return new ViewAssignmentPollOption(option, user);
|
return new ViewAssignmentPollOption(option, user);
|
||||||
});
|
})
|
||||||
|
.sort((a, b) => a.weight - b.weight);
|
||||||
return new ViewAssignmentPoll(poll, options);
|
return new ViewAssignmentPoll(poll, options);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds another user as a candidate
|
* Adds/removes another user to/from the candidates list of an assignment
|
||||||
*
|
*
|
||||||
* @param userId User id of a candidate
|
* @param user A ViewUser
|
||||||
* @param assignment The assignment to add the candidate to
|
* @param assignment The assignment to add the candidate to
|
||||||
|
* @param adding optional boolean to force an add (true)/ remove (false)
|
||||||
|
* of the candidate. Else, the candidate will be added if not on the list,
|
||||||
|
* and removed if on the list
|
||||||
*/
|
*/
|
||||||
public async changeCandidate(userId: number, assignment: ViewAssignment): Promise<void> {
|
public async changeCandidate(user: ViewUser, assignment: ViewAssignment, adding?: boolean): Promise<void> {
|
||||||
const data = { user: userId };
|
const data = { user: user.id };
|
||||||
if (assignment.candidates.some(candidate => candidate.id === userId)) {
|
if (assignment.candidates.some(candidate => candidate.id === user.id) && adding !== true) {
|
||||||
await this.httpService.delete(this.restPath + assignment.id + this.candidatureOtherPath, data);
|
await this.httpService.delete(this.restPath + assignment.id + this.candidatureOtherPath, data);
|
||||||
} else {
|
} else if (adding !== false) {
|
||||||
await this.httpService.post(this.restPath + assignment.id + this.candidatureOtherPath, data);
|
await this.httpService.post(this.restPath + assignment.id + this.candidatureOtherPath, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -149,6 +156,7 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito
|
|||||||
*/
|
*/
|
||||||
public async addPoll(assignment: ViewAssignment): Promise<void> {
|
public async addPoll(assignment: ViewAssignment): Promise<void> {
|
||||||
await this.httpService.post(this.restPath + assignment.id + this.createPollPath);
|
await this.httpService.post(this.restPath + assignment.id + this.createPollPath);
|
||||||
|
// TODO: change current tab to new poll
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -185,7 +193,7 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito
|
|||||||
const votes = poll.options.map(option => {
|
const votes = poll.options.map(option => {
|
||||||
switch (poll.pollmethod) {
|
switch (poll.pollmethod) {
|
||||||
case 'votes':
|
case 'votes':
|
||||||
return { Votes: option.votes.find(v => v.value === 'Yes').weight };
|
return { Votes: option.votes.find(v => v.value === 'Votes').weight };
|
||||||
case 'yn':
|
case 'yn':
|
||||||
return {
|
return {
|
||||||
Yes: option.votes.find(v => v.value === 'Yes').weight,
|
Yes: option.votes.find(v => v.value === 'Yes').weight,
|
||||||
@ -232,16 +240,14 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sorting the candidates
|
* Sends a request to sort an assignment's candidates
|
||||||
* TODO untested stub
|
|
||||||
*
|
*
|
||||||
* @param sortedCandidates
|
* @param sortedCandidates the id of the assignment related users (note: NOT viewUsers)
|
||||||
* @param assignment
|
* @param assignment
|
||||||
*/
|
*/
|
||||||
public async sortCandidates(sortedCandidates: any[], assignment: ViewAssignment): Promise<void> {
|
public async sortCandidates(sortedCandidates: number[], assignment: ViewAssignment): Promise<void> {
|
||||||
throw Error('TODO');
|
const restPath = `/rest/assignments/assignment/${assignment.id}/sort_related_users/`;
|
||||||
// const restPath = `/rest/assignments/assignment/${assignment.id}/sort_related_users`;
|
const data = { related_users: sortedCandidates };
|
||||||
// const data = { related_users: sortedCandidates };
|
await this.httpService.post(restPath, data);
|
||||||
// await this.httpService.post(restPath, data);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,7 @@ import { _ } from 'app/core/translate/translation-marker';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The possible keys of a poll object that represent numbers.
|
* The possible keys of a poll object that represent numbers.
|
||||||
* TODO Should be 'key of MotionPoll if type of key is number'
|
* TODO Should be 'key of MotionPoll|AssinmentPoll if type of key is number'
|
||||||
* TODO: normalize MotionPoll model and other poll models
|
|
||||||
*/
|
*/
|
||||||
export type CalculablePollKey = 'votesvalid' | 'votesinvalid' | 'votescast' | 'yes' | 'no' | 'abstain';
|
export type CalculablePollKey = 'votesvalid' | 'votesinvalid' | 'votescast' | 'yes' | 'no' | 'abstain';
|
||||||
|
|
||||||
@ -13,12 +12,12 @@ export type CalculablePollKey = 'votesvalid' | 'votesinvalid' | 'votescast' | 'y
|
|||||||
* TODO: may be obsolete if the server switches to lower case only
|
* TODO: may be obsolete if the server switches to lower case only
|
||||||
* (lower case variants are already in CalculablePollKey)
|
* (lower case variants are already in CalculablePollKey)
|
||||||
*/
|
*/
|
||||||
export type PollVoteValue = 'Yes' | 'No' | 'Abstain';
|
export type PollVoteValue = 'Yes' | 'No' | 'Abstain' | 'Votes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface representing possible majority calculation methods. The implementing
|
* Interface representing possible majority calculation methods. The implementing
|
||||||
* calc function should return an integer number that must be reached for the
|
* calc function should return an integer number that must be reached for the
|
||||||
* option to reach the quorum, or null if disabled
|
* option to successfully fulfill the quorum, or null if disabled
|
||||||
*/
|
*/
|
||||||
export interface MajorityMethod {
|
export interface MajorityMethod {
|
||||||
value: string;
|
value: string;
|
||||||
@ -33,17 +32,26 @@ export const PollMajorityMethod: MajorityMethod[] = [
|
|||||||
{
|
{
|
||||||
value: 'simple_majority',
|
value: 'simple_majority',
|
||||||
display_name: 'Simple majority',
|
display_name: 'Simple majority',
|
||||||
calc: base => Math.ceil(base * 0.5)
|
calc: base => {
|
||||||
|
const q = base * 0.5;
|
||||||
|
return Number.isInteger(q) ? q + 1 : Math.ceil(q);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'two-thirds_majority',
|
value: 'two-thirds_majority',
|
||||||
display_name: 'Two-thirds majority',
|
display_name: 'Two-thirds majority',
|
||||||
calc: base => Math.ceil((base / 3) * 2)
|
calc: base => {
|
||||||
|
const q = (base / 3) * 2;
|
||||||
|
return Number.isInteger(q) ? q + 1 : Math.ceil(q);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'three-quarters_majority',
|
value: 'three-quarters_majority',
|
||||||
display_name: 'Three-quarters majority',
|
display_name: 'Three-quarters majority',
|
||||||
calc: base => Math.ceil((base / 4) * 3)
|
calc: base => {
|
||||||
|
const q = (base / 4) * 3;
|
||||||
|
return Number.isInteger(q) ? q + 1 : Math.ceil(q);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'disabled',
|
value: 'disabled',
|
||||||
@ -95,6 +103,7 @@ export abstract class PollService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* empty constructor
|
* empty constructor
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
public constructor() {}
|
public constructor() {}
|
||||||
|
|
||||||
|
@ -8,10 +8,7 @@
|
|||||||
>
|
>
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<div class="title-slot">
|
<div class="title-slot">
|
||||||
<h2 *ngIf="assignment && !newAssignment">
|
<h2 *ngIf="!newAssignment" translate>Election</h2>
|
||||||
<span *ngIf="!editAssignment">{{ assignment.getTitle() }}</span>
|
|
||||||
<span *ngIf="editAssignment">{{ assignmentForm.get('title').value }}</span>
|
|
||||||
</h2>
|
|
||||||
<h2 *ngIf="newAssignment" translate>New election</h2>
|
<h2 *ngIf="newAssignment" translate>New election</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -23,17 +20,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<mat-menu #assignmentDetailMenu="matMenu">
|
<mat-menu #assignmentDetailMenu="matMenu">
|
||||||
<!-- delete -->
|
|
||||||
<!-- print, edit, delete -->
|
|
||||||
<div *ngIf="assignment">
|
<div *ngIf="assignment">
|
||||||
<!-- PDF -->
|
<!-- PDF -->
|
||||||
<button mat-menu-item (click)="onDownloadPdf()">
|
<button mat-menu-item (click)="onDownloadPdf()">
|
||||||
<!-- TODO: results or descritipon. Results if published -->
|
<!-- TODO: results or description. Results if published -->
|
||||||
<mat-icon>picture_as_pdf</mat-icon>
|
<mat-icon>picture_as_pdf</mat-icon>
|
||||||
<span translate>PDF</span>
|
<span translate>PDF</span>
|
||||||
</button>
|
</button>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<!-- Delete -->
|
<!-- Delete -->
|
||||||
|
</div>
|
||||||
|
<div *ngIf="assignment && hasPerms('manage')">
|
||||||
<button mat-menu-item class="red-warning-text" (click)="onDeleteAssignmentButton()">
|
<button mat-menu-item class="red-warning-text" (click)="onDeleteAssignmentButton()">
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon>delete</mat-icon>
|
||||||
<span translate>Delete</span>
|
<span translate>Delete</span>
|
||||||
@ -43,12 +40,6 @@
|
|||||||
</os-head-bar>
|
</os-head-bar>
|
||||||
|
|
||||||
<div class="content-container">
|
<div class="content-container">
|
||||||
<!-- Title -->
|
|
||||||
<div class="title on-transition-fade" *ngIf="assignment && !editAssignment">
|
|
||||||
<div class="title-line">
|
|
||||||
<h1>{{ assignment.getTitle() }}</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ng-container *ngIf="vp.isMobile; then mobileView; else desktopView"></ng-container>
|
<ng-container *ngIf="vp.isMobile; then mobileView; else desktopView"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -57,119 +48,130 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #desktopView>
|
<ng-template #desktopView>
|
||||||
<mat-card class="os-card">
|
|
||||||
<div *ngIf="editAssignment">
|
<div *ngIf="editAssignment">
|
||||||
<ng-container [ngTemplateOutlet]="assignmentFormTemplate"></ng-container>
|
<ng-container [ngTemplateOutlet]="assignmentFormTemplate"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!editAssignment">
|
<div *ngIf="!editAssignment">
|
||||||
<mat-card class="os-card">
|
|
||||||
<ng-container [ngTemplateOutlet]="metaInfoTemplate"></ng-container>
|
<ng-container [ngTemplateOutlet]="metaInfoTemplate"></ng-container>
|
||||||
</mat-card>
|
|
||||||
<mat-card class="os-card">
|
|
||||||
<ng-container [ngTemplateOutlet]="contentTemplate"></ng-container>
|
<ng-container [ngTemplateOutlet]="contentTemplate"></ng-container>
|
||||||
</mat-card>
|
|
||||||
</div>
|
</div>
|
||||||
</mat-card>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #metaInfoTemplate>
|
<ng-template #metaInfoTemplate>
|
||||||
<span translate>Meta information</span>
|
<mat-card class="os-card" *ngIf="assignment">
|
||||||
<div *ngIf="assignment; "flex-spaced"">
|
<h1>{{ assignment.getTitle() }}</h1>
|
||||||
|
<div *ngIf="assignment">
|
||||||
|
<div *ngIf="assignment.assignment.description" [innerHTML]="assignment.assignment.description"></div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span translate>Number of persons to be elected</span>:
|
<span translate>Number of persons to be elected</span>:
|
||||||
<span>{{ assignment.assignment.open_posts }}</span>
|
<span>{{ assignment.assignment.open_posts }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span> {{ phaseString | translate }}</span>
|
<span translate>Phase</span>:
|
||||||
<mat-form-field>
|
<mat-basic-chip *ngIf="hasPerms('manage')" [matMenuTriggerFor]="phaseMenu" class="bluegrey" disableRipple>
|
||||||
<mat-label translate>Phase</mat-label>
|
{{ assignment.phaseString | translate }}
|
||||||
<mat-select
|
</mat-basic-chip>
|
||||||
class="selection"
|
<mat-basic-chip *ngIf="!hasPerms('manage')" class="bluegrey" disableRipple>
|
||||||
[disabled]="!hasPerms('manage')"
|
{{ assignment.phaseString | translate }}
|
||||||
(selectionChange)="setPhase($event)"
|
</mat-basic-chip>
|
||||||
[value]="assignment.phase"
|
<mat-menu #phaseMenu="matMenu">
|
||||||
>
|
<button *ngFor="let option of phaseOptions" mat-menu-item (click)="onSetPhaseButton(option.value)">
|
||||||
<mat-option *ngFor="let option of phaseOptions" [value]="option.value">
|
|
||||||
{{ option.display_name | translate }}
|
{{ option.display_name | translate }}
|
||||||
</mat-option>
|
</button>
|
||||||
</mat-select>
|
</mat-menu>
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</mat-card>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #contentTemplate>
|
<ng-template #contentTemplate>
|
||||||
<ng-container *ngIf="assignment && assignment.phase !== 2" [ngTemplateOutlet]="candidatesTemplate">
|
<mat-card class="os-card">
|
||||||
|
<ng-container *ngIf="assignment && !assignment.isFinished" [ngTemplateOutlet]="candidatesTemplate">
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<!-- TODO related agenda item to create/updade: internal status; parent_id ? -->
|
<!-- TODO related agenda item to create/updade: internal status; parent_id ? -->
|
||||||
<ng-container [ngTemplateOutlet]="pollTemplate"></ng-container>
|
<ng-container [ngTemplateOutlet]="pollTemplate"></ng-container>
|
||||||
<!-- TODO different status/display if finished -->
|
<!-- TODO different status/display if finished -->
|
||||||
|
</mat-card>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<!-- poll template -->
|
<!-- poll template -->
|
||||||
<ng-template #pollTemplate>
|
<ng-template #pollTemplate>
|
||||||
<div>
|
<mat-tab-group
|
||||||
<!-- TODO: header. New poll button to the right, and with icon -->
|
(selectedTabChange)="onTabChange()"
|
||||||
<!-- new poll button -->
|
*ngIf="assignment && assignment.polls && assignment.polls.length"
|
||||||
<button type="button" mat-button *ngIf="hasPerms('createPoll')" (click)="createPoll()">
|
>
|
||||||
<span translate>Create poll</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<mat-tab-group (selectedTabChange)="onTabChange()" *ngIf="assignment && assignment.polls">
|
|
||||||
<!-- TODO avoid animation/switching on update -->
|
<!-- TODO avoid animation/switching on update -->
|
||||||
<mat-tab *ngFor="let poll of assignment.polls; let i = index" [label]="getPollLabel(poll, i)">
|
<mat-tab
|
||||||
|
*ngFor="let poll of assignment.polls; let i = index; trackBy: trackByIndex"
|
||||||
|
[label]="getPollLabel(poll, i)"
|
||||||
|
>
|
||||||
<os-assignment-poll [assignment]="assignment" [poll]="poll"> </os-assignment-poll>
|
<os-assignment-poll [assignment]="assignment" [poll]="poll"> </os-assignment-poll>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
</mat-tab-group>
|
</mat-tab-group>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #candidatesTemplate>
|
<ng-template #candidatesTemplate>
|
||||||
<!-- TODO two columns here: Left candidates; right add/remove -->
|
<!-- candidates -->
|
||||||
<div>
|
<div class="candidates-list" *ngIf="assignment && assignment.assignmentRelatedUsers && assignment.assignmentRelatedUsers.length > 0">
|
||||||
<div>
|
<os-sorting-list
|
||||||
<div *ngIf="assignment && assignment.candidates">
|
[input]="assignment.assignmentRelatedUsers"
|
||||||
<!-- TODO: Sorting -->
|
[live]="true"
|
||||||
<div *ngFor="let candidate of assignment.candidates">
|
[count]="true"
|
||||||
<span>{{ candidate.full_name }}</span>
|
[enable]="hasPerms('addOthers')"
|
||||||
<button mat-button *ngIf="hasPerms('addOthers')" (click)="removeUser(candidate)">Remove</button>
|
(sortEvent)="onSortingChange($event)"
|
||||||
|
>
|
||||||
|
<!-- implicit item references into the component using ng-template slot -->
|
||||||
|
<ng-template let-item>
|
||||||
|
<span *ngIf="hasPerms('addOthers')">
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
matTooltip="{{ 'Remove candidate' | translate }}"
|
||||||
|
(click)="removeUser(item)"
|
||||||
|
>
|
||||||
|
<mat-icon>clear</mat-icon>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</ng-template>
|
||||||
|
</os-sorting-list>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<!-- Search for candidates -->
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div *ngIf="hasPerms('addOthers')">
|
<div *ngIf="hasPerms('addOthers')">
|
||||||
<!-- candidates: if open: Select list, sortable (like list of speakers) -->
|
<form *ngIf="filteredCandidates && filteredCandidates.value.length > 0" [formGroup]="candidatesForm">
|
||||||
<os-search-value-selector
|
<os-search-value-selector
|
||||||
*ngIf="hasPerms('addOthers')"
|
*ngIf="hasPerms('addOthers')"
|
||||||
ngDefaultControl
|
ngDefaultControl
|
||||||
placeholder="Add candidate"
|
listname="{{ 'Select a new candidate' | translate }}"
|
||||||
[formControl]="candidatesForm.get('candidate')"
|
[formControl]="candidatesForm.get('userId')"
|
||||||
[InputListValues]="availableCandidates"
|
[InputListValues]="filteredCandidates"
|
||||||
[form]="candidatesForm"
|
[form]="candidatesForm"
|
||||||
|
[multiple]="false"
|
||||||
>
|
>
|
||||||
|
<!-- TODO: better class here: wider -->
|
||||||
|
<!-- TODO: Performance check: something seems off here with filteredCandidates -->
|
||||||
</os-search-value-selector>
|
</os-search-value-selector>
|
||||||
<button mat-button [disabled]="!candidatesForm.get('candidate').value" (click)="addUser()">
|
</form>
|
||||||
<span translate>add</span>
|
|
||||||
</button>
|
|
||||||
<!-- TODO disable if no user set; filter out users already in list of candidates -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<!-- Add me and remove me if OP has correct permission -->
|
||||||
|
<div *ngIf="assignment && hasPerms('addSelf') && assignment.candidates" class="add-self-buttons">
|
||||||
<!-- class="spacer-top-10" -->
|
<div>
|
||||||
<button
|
<button mat-stroked-button (click)="addSelf()" *ngIf="!isSelfCandidate">
|
||||||
type="button"
|
<mat-icon>add</mat-icon>
|
||||||
mat-button
|
<span translate>Add me</span>
|
||||||
color="primary"
|
|
||||||
*ngIf="hasPerms('addSelf') && !isSelfCandidate"
|
|
||||||
(click)="addSelf()"
|
|
||||||
>
|
|
||||||
<span class="upper" translate>Add me</span>
|
|
||||||
</button>
|
</button>
|
||||||
<br />
|
<button mat-stroked-button (click)="removeSelf()" *ngIf="isSelfCandidate">
|
||||||
<button type="button" mat-button *ngIf="hasPerms('addSelf') && isSelfCandidate" (click)="removeSelf()">
|
<mat-icon>remove</mat-icon>
|
||||||
<span translate>Remove me</span>
|
<span translate>Remove me</span>
|
||||||
</button>
|
</button>
|
||||||
<br />
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="assignment && hasPerms('createPoll')">
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<button mat-button (click)="createPoll()">
|
||||||
|
<mat-icon color="primary">poll</mat-icon>
|
||||||
|
<span translate>New poll</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<mat-divider *ngIf="assignment && assignment.polls && assignment.polls.length"> </mat-divider>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #assignmentFormTemplate>
|
<ng-template #assignmentFormTemplate>
|
||||||
@ -180,15 +182,17 @@
|
|||||||
(keydown)="onKeyDown($event)"
|
(keydown)="onKeyDown($event)"
|
||||||
*ngIf="assignment && editAssignment"
|
*ngIf="assignment && editAssignment"
|
||||||
>
|
>
|
||||||
|
<div>
|
||||||
<!-- title -->
|
<!-- title -->
|
||||||
<mat-form-field>
|
<mat-form-field class="full-width">
|
||||||
<input
|
<input
|
||||||
matInput
|
matInput
|
||||||
placeholder="{{ 'Title' | translate }}"
|
placeholder="{{ 'Title' | translate }}"
|
||||||
formControlName="title"
|
formControlName="title"
|
||||||
[value]="assignmentCopy.title || ''"
|
[value]="assignmentCopy.getTitle() || ''"
|
||||||
/>
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- description: HTML Editor -->
|
<!-- description: HTML Editor -->
|
||||||
<editor
|
<editor
|
||||||
@ -209,7 +213,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- searchValueSelector: tags -->
|
<!-- searchValueSelector: tags -->
|
||||||
<div class="content-field">
|
<div class="content-field" *ngIf="tagsAvailable">
|
||||||
<os-search-value-selector
|
<os-search-value-selector
|
||||||
ngDefaultControl
|
ngDefaultControl
|
||||||
[form]="assignmentForm"
|
[form]="assignmentForm"
|
||||||
@ -222,7 +226,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- searchValueSelector:agendaItem -->
|
<!-- searchValueSelector:agendaItem -->
|
||||||
<div class="content-field">
|
<div class="content-field" *ngIf="parentsAvailable">
|
||||||
<os-search-value-selector
|
<os-search-value-selector
|
||||||
ngDefaultControl
|
ngDefaultControl
|
||||||
[form]="assignmentForm"
|
[form]="assignmentForm"
|
||||||
@ -235,6 +239,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- poll_description_default -->
|
<!-- poll_description_default -->
|
||||||
|
<div>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<input
|
<input
|
||||||
matInput
|
matInput
|
||||||
@ -243,7 +248,9 @@
|
|||||||
[value]="assignmentCopy.assignment.poll_description_default || ''"
|
[value]="assignmentCopy.assignment.poll_description_default || ''"
|
||||||
/>
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
<!-- open posts: number -->
|
<!-- open posts: number -->
|
||||||
|
<div>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<input
|
<input
|
||||||
matInput
|
matInput
|
||||||
@ -252,6 +259,7 @@
|
|||||||
[value]="assignmentCopy.assignment.open_posts || null"
|
[value]="assignmentCopy.assignment.open_posts || null"
|
||||||
/>
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
<!-- TODO searchValueSelector: Parent -->
|
<!-- TODO searchValueSelector: Parent -->
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.candidates {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
.candidate {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
border-bottom: 1px solid lightgray;
|
||||||
|
vertical-align: top;
|
||||||
|
.name {
|
||||||
|
word-wrap: break-word;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-aligned {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: same as .waiting-list in list-of-speakers
|
||||||
|
.candidates-list {
|
||||||
|
padding: 10px 25px 0 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: duplicate in list-of-speakers
|
||||||
|
.add-self-buttons {
|
||||||
|
padding: 15px 0 20px 25px;
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||||
import { MatSnackBar, MatSelectChange } from '@angular/material';
|
import { MatSnackBar } from '@angular/material';
|
||||||
import { Router, ActivatedRoute } from '@angular/router';
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
@ -8,22 +8,22 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
import { BehaviorSubject } from 'rxjs';
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
import { Assignment } from 'app/shared/models/assignments/assignment';
|
import { Assignment } from 'app/shared/models/assignments/assignment';
|
||||||
|
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
||||||
import { AssignmentPollService } from '../../services/assignment-poll.service';
|
import { AssignmentPollService } from '../../services/assignment-poll.service';
|
||||||
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
|
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
|
||||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||||
import { ConstantsService } from 'app/core/core-services/constants.service';
|
|
||||||
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
|
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
|
||||||
import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service';
|
import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service';
|
||||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||||
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service';
|
import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service';
|
||||||
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
||||||
import { ViewAssignment, AssignmentPhase } from '../../models/view-assignment';
|
import { ViewAssignment, AssignmentPhases } from '../../models/view-assignment';
|
||||||
|
import { ViewAssignmentRelatedUser } from '../../models/view-assignment-related-user';
|
||||||
import { ViewItem } from 'app/site/agenda/models/view-item';
|
import { ViewItem } from 'app/site/agenda/models/view-item';
|
||||||
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||||
import { ViewTag } from 'app/site/tags/models/view-tag';
|
import { ViewTag } from 'app/site/tags/models/view-tag';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for the assignment detail view
|
* Component for the assignment detail view
|
||||||
@ -47,16 +47,17 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
/**
|
/**
|
||||||
* The different phases of an assignment. Info is fetched from server
|
* The different phases of an assignment. Info is fetched from server
|
||||||
*/
|
*/
|
||||||
public phaseOptions: AssignmentPhase[] = [];
|
public phaseOptions = AssignmentPhases;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of users (used in searchValueSelector for candidates)
|
* List of users available as candidates (used as raw data for {@link filteredCandidates})
|
||||||
* TODO Candidates already in the list should be filtered out
|
|
||||||
*/
|
*/
|
||||||
public availableCandidates = new BehaviorSubject<ViewUser[]>([]);
|
private availableCandidates = new BehaviorSubject<ViewUser[]>([]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO a filtered list (excluding users already in this.assignment.candidates)
|
* A BehaviourSubject with a filtered list of users (excluding users already
|
||||||
|
* in the list of candidates). It is updated each time {@link filterCandidates}
|
||||||
|
* is called (triggered by autoupdates)
|
||||||
*/
|
*/
|
||||||
public filteredCandidates = new BehaviorSubject<ViewUser[]>([]);
|
public filteredCandidates = new BehaviorSubject<ViewUser[]>([]);
|
||||||
|
|
||||||
@ -88,6 +89,8 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
*/
|
*/
|
||||||
public set assignment(assignment: ViewAssignment) {
|
public set assignment(assignment: ViewAssignment) {
|
||||||
this._assignment = assignment;
|
this._assignment = assignment;
|
||||||
|
|
||||||
|
this.filterCandidates();
|
||||||
if (this.assignment.polls.length) {
|
if (this.assignment.polls.length) {
|
||||||
this.assignment.polls.forEach(poll => {
|
this.assignment.polls.forEach(poll => {
|
||||||
poll.pollBase = this.pollService.getBaseAmount(poll);
|
poll.pollBase = this.pollService.getBaseAmount(poll);
|
||||||
@ -122,17 +125,21 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* gets the current assignment phase as string
|
* Checks if there are any tags available
|
||||||
*
|
|
||||||
* @returns a matching string (untranslated)
|
|
||||||
*/
|
*/
|
||||||
public get phaseString(): string {
|
public get tagsAvailable(): boolean {
|
||||||
const mapping = this.phaseOptions.find(ph => ph.value === this.assignment.phase);
|
return this.tagsObserver.getValue().length > 0;
|
||||||
return mapping ? mapping.display_name : '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor. Build forms and subscribe to needed configs, constants and updates
|
* Checks if there are any tags available
|
||||||
|
*/
|
||||||
|
public get parentsAvailable(): boolean {
|
||||||
|
return this.agendaObserver.getValue().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor. Build forms and subscribe to needed configs and updates
|
||||||
*
|
*
|
||||||
* @param title
|
* @param title
|
||||||
* @param translate
|
* @param translate
|
||||||
@ -145,7 +152,6 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
* @param formBuilder
|
* @param formBuilder
|
||||||
* @param repo
|
* @param repo
|
||||||
* @param userRepo
|
* @param userRepo
|
||||||
* @param constants
|
|
||||||
* @param pollService
|
* @param pollService
|
||||||
* @param agendaRepo
|
* @param agendaRepo
|
||||||
* @param tagRepo
|
* @param tagRepo
|
||||||
@ -163,17 +169,19 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
formBuilder: FormBuilder,
|
formBuilder: FormBuilder,
|
||||||
public repo: AssignmentRepositoryService,
|
public repo: AssignmentRepositoryService,
|
||||||
private userRepo: UserRepositoryService,
|
private userRepo: UserRepositoryService,
|
||||||
private constants: ConstantsService,
|
|
||||||
public pollService: AssignmentPollService,
|
public pollService: AssignmentPollService,
|
||||||
private agendaRepo: ItemRepositoryService,
|
private agendaRepo: ItemRepositoryService,
|
||||||
private tagRepo: TagRepositoryService,
|
private tagRepo: TagRepositoryService,
|
||||||
private promptService: PromptService
|
private promptService: PromptService
|
||||||
) {
|
) {
|
||||||
super(title, translate, matSnackBar);
|
super(title, translate, matSnackBar);
|
||||||
/* Server side constants for phases */
|
this.subscriptions.push(
|
||||||
this.constants.get<AssignmentPhase[]>('AssignmentPhases').subscribe(phases => (this.phaseOptions = phases));
|
|
||||||
/* List of eligible users */
|
/* List of eligible users */
|
||||||
this.userRepo.getViewModelListObservable().subscribe(users => this.availableCandidates.next(users));
|
this.userRepo.getViewModelListObservable().subscribe(users => {
|
||||||
|
this.availableCandidates.next(users);
|
||||||
|
this.filterCandidates();
|
||||||
|
})
|
||||||
|
);
|
||||||
this.assignmentForm = formBuilder.group({
|
this.assignmentForm = formBuilder.group({
|
||||||
phase: null,
|
phase: null,
|
||||||
tags_id: [],
|
tags_id: [],
|
||||||
@ -184,7 +192,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
agenda_item_id: '' // create agenda item
|
agenda_item_id: '' // create agenda item
|
||||||
});
|
});
|
||||||
this.candidatesForm = formBuilder.group({
|
this.candidatesForm = formBuilder.group({
|
||||||
candidate: null
|
userId: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,9 +209,9 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
* Permission check for interactions.
|
* Permission check for interactions.
|
||||||
*
|
*
|
||||||
* Current operations supported:
|
* Current operations supported:
|
||||||
* - addSelf: the user can add themself to the list of candidates
|
* - addSelf: the user can add/remove themself to the list of candidates
|
||||||
* - addOthers: the user can add other candidates
|
* - addOthers: the user can add/remove other candidates
|
||||||
* - createPoll: the user can add/edit election poll (requires candidates to be present)
|
* - createPoll: the user can add/edit an election poll (requires candidates to be present)
|
||||||
* - manage: the user has general manage permissions (i.e. editing the assignment metaInfo)
|
* - manage: the user has general manage permissions (i.e. editing the assignment metaInfo)
|
||||||
*
|
*
|
||||||
* @param operation the action requested
|
* @param operation the action requested
|
||||||
@ -213,20 +221,28 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
const isManager = this.operator.hasPerms('assignments.can_manage');
|
const isManager = this.operator.hasPerms('assignments.can_manage');
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
case 'addSelf':
|
case 'addSelf':
|
||||||
if (isManager && this.assignment.phase !== 2) {
|
if (isManager && !this.assignment.isFinished) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return this.assignment.phase === 0 && this.operator.hasPerms('assignments.can_nominate_self');
|
return (
|
||||||
|
this.assignment.isSearchingForCandidates &&
|
||||||
|
this.operator.hasPerms('assignments.can_nominate_self') &&
|
||||||
|
!this.assignment.isFinished
|
||||||
|
);
|
||||||
}
|
}
|
||||||
case 'addOthers':
|
case 'addOthers':
|
||||||
if (isManager && this.assignment.phase !== 2) {
|
if (isManager && !this.assignment.isFinished) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return this.assignment.phase === 0 && this.operator.hasPerms('assignments.can_nominate_others');
|
return (
|
||||||
|
this.assignment.isSearchingForCandidates &&
|
||||||
|
this.operator.hasPerms('assignments.can_nominate_others') &&
|
||||||
|
!this.assignment.isFinished
|
||||||
|
);
|
||||||
}
|
}
|
||||||
case 'createPoll':
|
case 'createPoll':
|
||||||
return (
|
return (
|
||||||
isManager && this.assignment && this.assignment.phase !== 2 && this.assignment.candidateAmount > 0
|
isManager && this.assignment && !this.assignment.isFinished && this.assignment.candidateAmount > 0
|
||||||
);
|
);
|
||||||
case 'manage':
|
case 'manage':
|
||||||
return isManager;
|
return isManager;
|
||||||
@ -261,6 +277,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
private patchForm(assignment: ViewAssignment): void {
|
private patchForm(assignment: ViewAssignment): void {
|
||||||
this.assignmentCopy = assignment;
|
this.assignmentCopy = assignment;
|
||||||
this.assignmentForm.patchValue({
|
this.assignmentForm.patchValue({
|
||||||
|
title: assignment.title || '',
|
||||||
tags_id: assignment.assignment.tags_id || [],
|
tags_id: assignment.assignment.tags_id || [],
|
||||||
agendaItem: assignment.assignment.agenda_item_id || null,
|
agendaItem: assignment.assignment.agenda_item_id || null,
|
||||||
phase: assignment.phase, // todo default: 0?
|
phase: assignment.phase, // todo default: 0?
|
||||||
@ -304,23 +321,24 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a user to the list of candidates
|
* Adds the user from the candidates form to the list of candidates
|
||||||
|
*
|
||||||
|
* @param userId the id of a ViewUser
|
||||||
*/
|
*/
|
||||||
public async addUser(): Promise<void> {
|
public async addUser(userId: number): Promise<void> {
|
||||||
const candId = this.candidatesForm.get('candidate').value;
|
const user = this.userRepo.getViewModel(userId);
|
||||||
this.candidatesForm.setValue({ candidate: null });
|
if (user) {
|
||||||
if (candId) {
|
await this.repo.changeCandidate(user, this.assignment, true).then(null, this.raiseError);
|
||||||
await this.repo.changeCandidate(candId, this.assignment).then(null, this.raiseError);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a user from the list of candidates
|
* Removes a user from the list of candidates
|
||||||
*
|
*
|
||||||
* @param user Assignment User
|
* @param candidate A ViewAssignmentUser currently in the list of related users
|
||||||
*/
|
*/
|
||||||
public async removeUser(user: ViewUser): Promise<void> {
|
public async removeUser(candidate: ViewAssignmentRelatedUser): Promise<void> {
|
||||||
await this.repo.changeCandidate(user.id, this.assignment).then(null, this.raiseError);
|
await this.repo.changeCandidate(candidate.user, this.assignment, false).then(null, this.raiseError);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -342,6 +360,14 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.candidatesForm.valueChanges.subscribe(formResult => {
|
||||||
|
// resetting a form triggers a form.next(null) - check if data is present
|
||||||
|
if (formResult && formResult.userId) {
|
||||||
|
this.addUser(formResult.userId);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.newAssignment = true;
|
this.newAssignment = true;
|
||||||
// TODO set defaults?
|
// TODO set defaults?
|
||||||
@ -357,8 +383,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
public async onDeleteAssignmentButton(): Promise<void> {
|
public async onDeleteAssignmentButton(): Promise<void> {
|
||||||
const title = this.translate.instant('Are you sure you want to delete this election?');
|
const title = this.translate.instant('Are you sure you want to delete this election?');
|
||||||
if (await this.promptService.open(title, this.assignment.getTitle())) {
|
if (await this.promptService.open(title, this.assignment.getTitle())) {
|
||||||
await this.repo.delete(this.assignment);
|
this.repo.delete(this.assignment).then(() => this.router.navigate(['../']), this.raiseError);
|
||||||
this.router.navigate(['../assignments/']);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -374,12 +399,10 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
* TODO: only with existing assignments, else it should fail
|
* TODO: only with existing assignments, else it should fail
|
||||||
* TODO check permissions and conditions
|
* TODO check permissions and conditions
|
||||||
*
|
*
|
||||||
* @param event
|
* @param value the phase to set
|
||||||
*/
|
*/
|
||||||
public async setPhase(event: MatSelectChange): Promise<void> {
|
public async onSetPhaseButton(value: number): Promise<void> {
|
||||||
if (!this.newAssignment && this.phaseOptions.find(option => option.value === event.value)) {
|
this.repo.update({ phase: value }, this.assignment).then(null, this.raiseError);
|
||||||
this.repo.update({ phase: event.value }, this.assignment).then(null, this.raiseError);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onDownloadPdf(): void {
|
public onDownloadPdf(): void {
|
||||||
@ -426,11 +449,48 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Assemble a meaningful label for the poll
|
* Assemble a meaningful label for the poll
|
||||||
* TODO (currently e.g. 'Ballot 10 (unublished)')
|
* Published polls will look like 'Ballot 2 (published)'
|
||||||
|
* other polls will be named 'Ballot 2' for normal users, with the hint
|
||||||
|
* '(unpulished)' appended for manager users
|
||||||
|
*
|
||||||
|
* @param poll
|
||||||
|
* @param index the index of the poll relative to the assignment
|
||||||
*/
|
*/
|
||||||
public getPollLabel(poll: AssignmentPoll, index: number): string {
|
public getPollLabel(poll: AssignmentPoll, index: number): string {
|
||||||
const pubState = poll.published ? this.translate.instant('published') : this.translate.instant('unpublished');
|
const title = `${this.translate.instant('Ballot')} ${index + 1}`;
|
||||||
const title = this.translate.instant('Ballot');
|
if (poll.published) {
|
||||||
return `${title} ${index + 1} (${pubState})`;
|
return title + ` (${this.translate.instant('published')})`;
|
||||||
|
} else {
|
||||||
|
if (this.hasPerms('manage')) {
|
||||||
|
return title + ` (${this.translate.instant('unpublished')})`;
|
||||||
|
} else {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers an update of the filter for the list of available candidates
|
||||||
|
* (triggered on an autoupdate of either users or the assignment)
|
||||||
|
*/
|
||||||
|
private filterCandidates(): void {
|
||||||
|
if (!this.assignment || !this.assignment.candidates) {
|
||||||
|
this.filteredCandidates.next(this.availableCandidates.getValue());
|
||||||
|
} else {
|
||||||
|
this.filteredCandidates.next(
|
||||||
|
this.availableCandidates
|
||||||
|
.getValue()
|
||||||
|
.filter(u => !this.assignment.candidates.some(cand => cand.id === u.id))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers an update of the sorting.
|
||||||
|
*/
|
||||||
|
public onSortingChange(listInNewOrder: ViewAssignmentRelatedUser[]): void {
|
||||||
|
this.repo
|
||||||
|
.sortCandidates(listInNewOrder.map(relatedUser => relatedUser.id), this.assignment)
|
||||||
|
.then(null, this.raiseError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,12 +40,12 @@
|
|||||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Title</mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header>Title</mat-header-cell>
|
||||||
<mat-cell *matCellDef="let assignment">{{ assignment.getListTitle() }}</mat-cell>
|
<mat-cell *matCellDef="let assignment">{{ assignment.getListTitle() }}</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<!-- pahse column-->
|
<!-- phase column-->
|
||||||
<ng-container matColumnDef="phase">
|
<ng-container matColumnDef="phase">
|
||||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Phase</mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header>Phase</mat-header-cell>
|
||||||
<mat-cell *matCellDef="let assignment">
|
<mat-cell *matCellDef="let assignment">
|
||||||
<mat-chip-list>
|
<mat-chip-list>
|
||||||
<mat-chip color="primary" selected>{{ assignment.phase }}</mat-chip>
|
<mat-chip color="primary" selected>{{ assignment.phaseString | translate }}</mat-chip>
|
||||||
</mat-chip-list>
|
</mat-chip-list>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
<button mat-menu-item (click)="selectAll()">
|
<button mat-menu-item (click)="selectAll()">
|
||||||
@ -62,7 +62,7 @@
|
|||||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Candidates</mat-header-cell>
|
<mat-header-cell *matHeaderCellDef mat-sort-header>Candidates</mat-header-cell>
|
||||||
<mat-cell *matCellDef="let assignment">
|
<mat-cell *matCellDef="let assignment">
|
||||||
<mat-chip-list>
|
<mat-chip-list>
|
||||||
<mat-chip color="accent" selected>{{ assignment.candidateAmount }}</mat-chip>
|
<mat-chip color="accent" selected matTooltip="{{ 'Number of candidates' | translate }}">{{ assignment.candidateAmount }}</mat-chip>
|
||||||
</mat-chip-list>
|
</mat-chip-list>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { AssignmentListComponent } from './assignment-list.component';
|
import { AssignmentListComponent } from './assignment-list.component';
|
||||||
import { E2EImportsModule } from '../../../../../e2e-imports.module';
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
describe('AssignmentListComponent', () => {
|
describe('AssignmentListComponent', () => {
|
||||||
let component: AssignmentListComponent;
|
let component: AssignmentListComponent;
|
||||||
|
@ -13,7 +13,7 @@ import { ListViewBaseComponent } from 'app/site/base/list-view-base';
|
|||||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
import { StorageService } from 'app/core/core-services/storage.service';
|
import { StorageService } from 'app/core/core-services/storage.service';
|
||||||
import { ViewAssignment } from '../../models/view-assignment';
|
import { ViewAssignment, AssignmentPhases } from '../../models/view-assignment';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List view for the assignments
|
* List view for the assignments
|
||||||
@ -24,6 +24,11 @@ import { ViewAssignment } from '../../models/view-assignment';
|
|||||||
styleUrls: ['./assignment-list.component.scss']
|
styleUrls: ['./assignment-list.component.scss']
|
||||||
})
|
})
|
||||||
export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignment, Assignment> implements OnInit {
|
export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignment, Assignment> implements OnInit {
|
||||||
|
/**
|
||||||
|
* The different phases of an assignment. Info is fetched from server
|
||||||
|
*/
|
||||||
|
public phaseOptions = AssignmentPhases;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
*
|
*
|
||||||
|
@ -1,73 +1,40 @@
|
|||||||
<h2 translate>Voting result</h2>
|
<h2 translate>Voting result</h2>
|
||||||
<div class="meta-text">
|
<div class="meta-text">
|
||||||
<span translate>Special values</span>:<br />
|
<span translate>Special values</span>:<br />
|
||||||
<mat-chip>-1</mat-chip> = <span translate>majority</span><br />
|
<mat-chip>-1</mat-chip> = <span translate>majority</span>
|
||||||
<mat-chip color="accent">-2</mat-chip> =
|
<mat-chip color="accent">-2</mat-chip> =
|
||||||
<span translate>undocumented</span>
|
<span translate>undocumented</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="spacer-top-10"></div>
|
||||||
<div class="flex-spaced" *ngFor="let candidate of data.poll.options">
|
<div class="width-600">
|
||||||
<div>
|
<!-- Candidate values -->
|
||||||
{{ getName(candidate.candidate_id) }}
|
<div [ngClass]="getGridClass()" *ngFor="let candidate of data.options">
|
||||||
|
<div class="candidate-name">
|
||||||
|
{{ candidate.user.full_name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="votes">
|
<div *ngFor="let key of optionPollKeys" class="votes">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<input
|
<input type="number"
|
||||||
type="number"
|
|
||||||
matInput
|
matInput
|
||||||
[value]="getValue('Yes', candidate)"
|
[value]="getValue(key, candidate)"
|
||||||
(change)="setValue('Yes', candidate, $event.target.value)"
|
(change)="setValue(key, candidate, $event.target.value)"
|
||||||
/>
|
/>
|
||||||
<mat-label *ngIf="data.poll.pollmethod !== 'votes'" translate>Yes</mat-label>
|
<mat-label> {{ key | translate }}</mat-label>
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field *ngIf="data.poll.pollmethod !== 'votes'">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
matInput
|
|
||||||
[value]="getValue('No', candidate)"
|
|
||||||
(change)="setValue('No', candidate, $event.target.value)"
|
|
||||||
/>
|
|
||||||
<mat-label translate>No</mat-label>
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field *ngIf="data.poll.pollmethod === 'yna'">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
matInput
|
|
||||||
[value]="getValue('Abstain', candidate)"
|
|
||||||
(change)="setValue('Abstain', candidate, $event.target.value)"
|
|
||||||
/>
|
|
||||||
<mat-label translate>Abstain</mat-label>
|
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider *ngIf="data.poll.pollmethod !== 'votes'"></mat-divider>
|
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Summary values -->
|
||||||
|
<div *ngFor="let sumValue of sumValues" class="sum-value">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
matInput
|
matInput
|
||||||
[value]="getSumValue('votesvalid')"
|
[value]="getSumValue(sumValue)"
|
||||||
(change)="setSumValue('votesvalid', $event.target.value)"
|
(change)="setSumValue(sumValue, $event.target.value)"
|
||||||
/>
|
/>
|
||||||
<mat-label translate>Valid votes</mat-label>
|
<mat-label>{{ pollService.getLabel(sumValue) }}</mat-label>
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
matInput
|
|
||||||
[value]="getSumValue('votesinvalid')"
|
|
||||||
(change)="setSumValue('votesinvalid', $event.target.value)"
|
|
||||||
/>
|
|
||||||
<mat-label translate>Invalid votes</mat-label>
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
matInput
|
|
||||||
[value]="getSumValue('votescast')"
|
|
||||||
(change)="setSumValue('votescast', $event.target.value)"
|
|
||||||
/>
|
|
||||||
<mat-label translate>Total votes</mat-label>
|
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="submit-buttons">
|
<div class="submit-buttons">
|
||||||
<button mat-button (click)="submit()">{{ 'Save' | translate }}</button>
|
<button mat-button (click)="submit()">{{ 'Save' | translate }}</button>
|
||||||
|
@ -13,7 +13,52 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.votes {
|
.candidate-name {
|
||||||
display: flex;
|
word-wrap: break-word;
|
||||||
justify-content: space-between;
|
width: 100%;
|
||||||
|
border-bottom: 1px solid grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.votes-grid-1 {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
grid-template-columns: auto 60px;
|
||||||
|
align-items: center;
|
||||||
|
.mat-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: more elegant way. Only grid-template-columns is different
|
||||||
|
.votes-grid-2 {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
align-items: center;
|
||||||
|
grid-template-columns: auto 60px 60px;
|
||||||
|
.mat-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: more elegant way. Only grid-template-columns is different
|
||||||
|
.votes-grid-3 {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
align-items: center;
|
||||||
|
grid-template-columns: auto 60px 60px 60px;
|
||||||
|
.mat-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sum-value {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.width-600 {
|
||||||
|
max-width: 600px;
|
||||||
}
|
}
|
||||||
|
@ -2,16 +2,17 @@ import { Component, Inject } from '@angular/core';
|
|||||||
import { MatDialogRef, MAT_DIALOG_DATA, MatSnackBar } from '@angular/material';
|
import { MatDialogRef, MAT_DIALOG_DATA, MatSnackBar } from '@angular/material';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { AssignmentPollOption } from 'app/shared/models/assignments/assignment-poll-option';
|
||||||
import { AssignmentPollService } from '../../services/assignment-poll.service';
|
import { AssignmentPollService } from '../../services/assignment-poll.service';
|
||||||
import { CalculablePollKey, PollVoteValue } from 'app/core/ui-services/poll.service';
|
import { CalculablePollKey, PollVoteValue } from 'app/core/ui-services/poll.service';
|
||||||
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||||
import { AssignmentPollOption } from 'app/shared/models/assignments/assignment-poll-option';
|
import { ViewAssignmentPollOption } from '../../models/view-assignment-poll-option';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vote entries included once for summary (e.g. total votes cast)
|
* Vote entries included once for summary (e.g. total votes cast)
|
||||||
*/
|
*/
|
||||||
type summaryPollKeys = 'votescast' | 'votesvalid' | 'votesinvalid';
|
type summaryPollKey = 'votescast' | 'votesvalid' | 'votesinvalid';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A dialog for updating the values of an assignment-related poll.
|
* A dialog for updating the values of an assignment-related poll.
|
||||||
@ -22,6 +23,11 @@ type summaryPollKeys = 'votescast' | 'votesvalid' | 'votesinvalid';
|
|||||||
styleUrls: ['./assignment-poll-dialog.component.scss']
|
styleUrls: ['./assignment-poll-dialog.component.scss']
|
||||||
})
|
})
|
||||||
export class AssignmentPollDialogComponent {
|
export class AssignmentPollDialogComponent {
|
||||||
|
/**
|
||||||
|
* The summary values that will have fields in the dialog
|
||||||
|
*/
|
||||||
|
public sumValues: summaryPollKey[] = ['votesvalid', 'votesinvalid', 'votescast'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of accepted special non-numerical values.
|
* List of accepted special non-numerical values.
|
||||||
* See {@link PollService.specialPollVotes}
|
* See {@link PollService.specialPollVotes}
|
||||||
@ -40,15 +46,16 @@ export class AssignmentPollDialogComponent {
|
|||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
public dialogRef: MatDialogRef<AssignmentPollDialogComponent>,
|
public dialogRef: MatDialogRef<AssignmentPollDialogComponent>,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: { poll: AssignmentPoll; users: ViewUser[] },
|
@Inject(MAT_DIALOG_DATA) public data: ViewAssignmentPoll,
|
||||||
private matSnackBar: MatSnackBar,
|
private matSnackBar: MatSnackBar,
|
||||||
private translate: TranslateService,
|
private translate: TranslateService,
|
||||||
private pollService: AssignmentPollService
|
public pollService: AssignmentPollService,
|
||||||
|
private userRepo: UserRepositoryService
|
||||||
) {
|
) {
|
||||||
this.specialValues = this.pollService.specialPollVotes;
|
this.specialValues = this.pollService.specialPollVotes;
|
||||||
switch (this.data.poll.pollmethod) {
|
switch (this.data.pollmethod) {
|
||||||
case 'votes':
|
case 'votes':
|
||||||
this.optionPollKeys = ['Yes'];
|
this.optionPollKeys = ['Votes'];
|
||||||
break;
|
break;
|
||||||
case 'yn':
|
case 'yn':
|
||||||
this.optionPollKeys = ['Yes', 'No'];
|
this.optionPollKeys = ['Yes', 'No'];
|
||||||
@ -73,7 +80,7 @@ export class AssignmentPollDialogComponent {
|
|||||||
* TODO better validation
|
* TODO better validation
|
||||||
*/
|
*/
|
||||||
public submit(): void {
|
public submit(): void {
|
||||||
const error = this.data.poll.options.find(dataoption => {
|
const error = this.data.options.find(dataoption => {
|
||||||
for (const key of this.optionPollKeys) {
|
for (const key of this.optionPollKeys) {
|
||||||
const keyValue = dataoption.votes.find(o => o.value === key);
|
const keyValue = dataoption.votes.find(o => o.value === key);
|
||||||
if (!keyValue || keyValue.weight === undefined) {
|
if (!keyValue || keyValue.weight === undefined) {
|
||||||
@ -90,7 +97,7 @@ export class AssignmentPollDialogComponent {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.dialogRef.close(this.data.poll);
|
this.dialogRef.close(this.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,18 +111,6 @@ export class AssignmentPollDialogComponent {
|
|||||||
return this.pollService.getLabel(key);
|
return this.pollService.getLabel(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the (full) name of a pollOption candidate
|
|
||||||
*
|
|
||||||
* @param the id of the candidate
|
|
||||||
* @returns the full_name property
|
|
||||||
*/
|
|
||||||
public getName(candidateId: number): string {
|
|
||||||
const user = this.data.users.find(c => c.id === candidateId);
|
|
||||||
return user ? user.full_name : 'unknown user';
|
|
||||||
// TODO error handling
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates a vote value
|
* Updates a vote value
|
||||||
*
|
*
|
||||||
@ -123,14 +118,14 @@ export class AssignmentPollDialogComponent {
|
|||||||
* @param candidate the candidate for whom to update the value
|
* @param candidate the candidate for whom to update the value
|
||||||
* @param newData the new value
|
* @param newData the new value
|
||||||
*/
|
*/
|
||||||
public setValue(value: PollVoteValue, candidate: AssignmentPollOption, newData: string): void {
|
public setValue(value: PollVoteValue, candidate: ViewAssignmentPollOption, newData: string): void {
|
||||||
const vote = candidate.votes.find(v => v.value === value);
|
const vote = candidate.votes.find(v => v.value === value);
|
||||||
if (vote) {
|
if (vote) {
|
||||||
vote.weight = parseInt(newData, 10);
|
vote.weight = parseFloat(newData);
|
||||||
} else {
|
} else {
|
||||||
candidate.votes.push({
|
candidate.votes.push({
|
||||||
value: value,
|
value: value,
|
||||||
weight: parseInt(newData, 10)
|
weight: parseFloat(newData)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -153,8 +148,8 @@ export class AssignmentPollDialogComponent {
|
|||||||
* @param value
|
* @param value
|
||||||
* @returns integer or undefined
|
* @returns integer or undefined
|
||||||
*/
|
*/
|
||||||
public getSumValue(value: summaryPollKeys): number | undefined {
|
public getSumValue(value: summaryPollKey): number | undefined {
|
||||||
return this.data.poll[value] || undefined;
|
return this.data[value] || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -163,7 +158,23 @@ export class AssignmentPollDialogComponent {
|
|||||||
* @param value
|
* @param value
|
||||||
* @param weight
|
* @param weight
|
||||||
*/
|
*/
|
||||||
public setSumValue(value: summaryPollKeys, weight: string): void {
|
public setSumValue(value: summaryPollKey, weight: string): void {
|
||||||
this.data.poll[value] = parseInt(weight, 10);
|
this.data[value] = parseFloat(weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getGridClass(): string {
|
||||||
|
return `votes-grid-${this.optionPollKeys.length}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the name for a poll option
|
||||||
|
* TODO: observable. Note that the assignment.related_user may not contain the user (anymore?)
|
||||||
|
*
|
||||||
|
* @param option Any poll option
|
||||||
|
* @returns the full_name for the candidate
|
||||||
|
*/
|
||||||
|
public getCandidateName(option: AssignmentPollOption): string {
|
||||||
|
const user = this.userRepo.getViewModel(option.candidate_id);
|
||||||
|
return user ? user.full_name : '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,53 +1,96 @@
|
|||||||
<!-- label: 'Ballot 1 ...' -->
|
<mat-card class="os-card" *ngIf="poll">
|
||||||
<h3 translate>Ballot</h3>
|
<div class="flex-spaced poll-menu">
|
||||||
<mat-card class="os-card">
|
|
||||||
<div class="flex-spaced">
|
|
||||||
<div>
|
|
||||||
<!-- *ngIf="poll.description"> -->
|
|
||||||
Description
|
|
||||||
<!-- {{ poll.description}} -->
|
|
||||||
</div>
|
|
||||||
<!-- Buttons -->
|
<!-- Buttons -->
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
*osPerms="'assignments.can_manage'; "core.can_manage_projector""
|
||||||
|
[matMenuTriggerFor]="pollItemMenu"
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
|
>
|
||||||
|
<mat-icon>more_vert</mat-icon>
|
||||||
|
</button>
|
||||||
|
<mat-menu #pollItemMenu="matMenu">
|
||||||
<div *osPerms="'assignments.can_manage'">
|
<div *osPerms="'assignments.can_manage'">
|
||||||
<button mat-button (click)="printBallot()">
|
<button mat-menu-item (click)="printBallot()">
|
||||||
|
<mat-icon>local_printshop</mat-icon>
|
||||||
<span translate>Print ballot paper</span>
|
<span translate>Print ballot paper</span>
|
||||||
</button>
|
</button>
|
||||||
<button mat-button [disabled]="assignment.phase == 2" (click)="enterVotes()">
|
<button mat-menu-item *ngIf="!assignment.isFinished" (click)="enterVotes()">
|
||||||
|
<mat-icon>edit</mat-icon>
|
||||||
<span translate>Enter votes</span>
|
<span translate>Enter votes</span>
|
||||||
</button>
|
</button>
|
||||||
<button mat-button (click)="togglePublished()">
|
<button mat-menu-item (click)="togglePublished()">
|
||||||
|
<mat-icon>
|
||||||
|
{{ poll.published ? 'visibility_off' : 'visibility' }}
|
||||||
|
</mat-icon>
|
||||||
<span *ngIf="!poll.published" translate>Publish</span>
|
<span *ngIf="!poll.published" translate>Publish</span>
|
||||||
<span *ngIf="poll.published" translate>Unpublish</span>
|
<span *ngIf="poll.published" translate>Unpublish</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div *osPerms="'core.can_manage_projector'">
|
<div *osPerms="'core.can_manage_projector'">
|
||||||
<button mat-button>
|
<button mat-menu-item>
|
||||||
|
<mat-icon>videocam</mat-icon>
|
||||||
<span translate>Project</span>
|
<span translate>Project</span>
|
||||||
<!-- os-projector-button ?-->
|
<!-- os-projector-button ?-->
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div *osPerms="'assignments.can_manage'">
|
<div *osPerms="'assignments.can_manage'">
|
||||||
<button mat-button class="red-warning-text" (click)="onDeletePoll(poll)">
|
<mat-divider></mat-divider>
|
||||||
|
<button mat-menu-item class="red-warning-text" (click)="onDeletePoll()">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
<span translate>Delete</span>
|
<span translate>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</mat-menu>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<!-- text input 'set hint for pballot paper' -->
|
<h4>
|
||||||
<!-- submit: setBallotHint(poll) -->
|
<span translate>Poll description</span>
|
||||||
|
</h4>
|
||||||
|
<div [formGroup]="descriptionForm">
|
||||||
|
<mat-form-field class="wide">
|
||||||
|
<input matInput formControlName="description" [disabled]="!canManage" />
|
||||||
|
</mat-form-field>
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
[disabled]="!dirtyDescription"
|
||||||
|
*ngIf="canManage"
|
||||||
|
(click)="onEditDescriptionButton()"
|
||||||
|
>
|
||||||
|
<mat-icon inline>edit</mat-icon>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="spacer-bottom-10">
|
||||||
|
<h4 translate>Poll method</h4>
|
||||||
|
<span>{{ pollMethodName | translate }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="on-transition-fade" *ngIf="poll.options">
|
||||||
|
<div *ngIf="pollData">
|
||||||
|
<div class="poll-grid">
|
||||||
|
<div></div>
|
||||||
|
<div><span class="table-view-list-title" translate>Candidate</span></div>
|
||||||
|
<div><span class="table-view-list-title" translate>Votes</span></div>
|
||||||
<div *ngIf="pollService.majorityMethods && majorityChoice">
|
<div *ngIf="pollService.majorityMethods && majorityChoice">
|
||||||
<span translate>Majority method</span>
|
<div>
|
||||||
|
<span class="table-view-list-title" translate>Quorum</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<!-- manager majority chip (menu trigger) -->
|
||||||
<mat-basic-chip *ngIf="canManage" [matMenuTriggerFor]="majorityMenu" class="grey" disableRipple>
|
<mat-basic-chip *ngIf="canManage" [matMenuTriggerFor]="majorityMenu" class="grey" disableRipple>
|
||||||
{{ majorityChoice.display_name | translate }}
|
{{ majorityChoice.display_name | translate }}
|
||||||
</mat-basic-chip>
|
</mat-basic-chip>
|
||||||
|
<!-- non-manager (menuless) majority chip -->
|
||||||
<mat-basic-chip *ngIf="!canManage" class="grey" disableRipple>
|
<mat-basic-chip *ngIf="!canManage" class="grey" disableRipple>
|
||||||
{{ majorityChoice.display_name | translate }}
|
{{ majorityChoice.display_name | translate }}
|
||||||
</mat-basic-chip>
|
</mat-basic-chip>
|
||||||
|
<!-- menu for selecting quorum choices -->
|
||||||
<mat-menu #majorityMenu="matMenu">
|
<mat-menu #majorityMenu="matMenu">
|
||||||
<button mat-menu-item *ngFor="let method of pollService.majorityMethods" (click)="setMajority(method)">
|
<button
|
||||||
|
mat-menu-item
|
||||||
|
*ngFor="let method of pollService.majorityMethods"
|
||||||
|
(click)="setMajority(method)"
|
||||||
|
>
|
||||||
<mat-icon *ngIf="method.value === majorityChoice.value">
|
<mat-icon *ngIf="method.value === majorityChoice.value">
|
||||||
check
|
check
|
||||||
</mat-icon>
|
</mat-icon>
|
||||||
@ -55,22 +98,46 @@
|
|||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</div>
|
</div>
|
||||||
<div class="on-transition-fade" *ngIf="poll && poll.options">
|
</div>
|
||||||
<div *ngFor="let option of poll.options" class="flex-spaced">
|
</div>
|
||||||
<div *ngIf="poll.published">
|
<div *ngFor="let option of poll.options" class="poll-grid poll-border">
|
||||||
<mat-checkbox [checked]="option.is_elected" (change)="toggleElected(option)">
|
<div>
|
||||||
<!-- TODO mark/unkmark elected osPerms -->
|
<div>
|
||||||
</mat-checkbox>
|
<button
|
||||||
|
type="button"
|
||||||
|
mat-icon-button
|
||||||
|
(click)="toggleElected(option)"
|
||||||
|
[disabled]="!canManage || assignment.isFinished"
|
||||||
|
disableRipple
|
||||||
|
>
|
||||||
|
<mat-icon
|
||||||
|
*ngIf="option.is_elected"
|
||||||
|
class="top-aligned green-text"
|
||||||
|
matTooltip="{{ 'Elected' | translate }}"
|
||||||
|
>check_box</mat-icon
|
||||||
|
>
|
||||||
|
<mat-icon
|
||||||
|
*ngIf="!option.is_elected && canManage && !assignment.isFinished"
|
||||||
|
class="top-aligned primary"
|
||||||
|
matTooltip="{{ 'Not elected' | translate }}"
|
||||||
|
>
|
||||||
|
check_box_outline_blank</mat-icon
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- candidate Name -->
|
<!-- candidate Name -->
|
||||||
<div>
|
<div class="candidate-name">
|
||||||
{{ option.user.full_name }}
|
{{ option.user.full_name }}
|
||||||
</div>
|
</div>
|
||||||
<!-- Votes -->
|
<!-- Votes -->
|
||||||
<div *ngIf="poll.published && poll.has_votes">
|
<div>
|
||||||
<div *ngFor="let vote of option.votes">
|
<div *ngIf="poll.has_votes">
|
||||||
|
<div *ngFor="let vote of option.votes" class="spacer-bottom-10">
|
||||||
<div class="poll-progress on-transition-fade">
|
<div class="poll-progress on-transition-fade">
|
||||||
<span>{{ pollService.getLabel(vote.value) | translate }}:</span>
|
<span *ngIf="vote.value !== 'Votes'"
|
||||||
|
>{{ pollService.getLabel(vote.value) | translate }}:</span
|
||||||
|
>
|
||||||
{{ pollService.getSpecialLabel(vote.weight) }}
|
{{ pollService.getSpecialLabel(vote.weight) }}
|
||||||
<span *ngIf="!pollService.isAbstractOption(poll, option)"
|
<span *ngIf="!pollService.isAbstractOption(poll, option)"
|
||||||
>({{ pollService.getPercent(poll, option, vote.value) }}%)</span
|
>({{ pollService.getPercent(poll, option, vote.value) }}%)</span
|
||||||
@ -86,35 +153,46 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
<div
|
<div
|
||||||
*ngIf="
|
*ngIf="
|
||||||
poll.has_votes &&
|
poll.has_votes &&
|
||||||
poll.published &&
|
|
||||||
majorityChoice &&
|
majorityChoice &&
|
||||||
majorityChoice.value !== 'disabled' &&
|
majorityChoice.value !== 'disabled' &&
|
||||||
!pollService.isAbstractOption(poll, option)
|
!pollService.isAbstractOption(poll, option)
|
||||||
"
|
"
|
||||||
|
class="poll-quorum"
|
||||||
>
|
>
|
||||||
<span>{{ pollService.yesQuorum(majorityChoice, poll, option) }}</span>
|
<span>{{ pollService.yesQuorum(majorityChoice, poll, option) }}</span>
|
||||||
<span *ngIf="quorumReached(option)" class="green-text">
|
<span [ngClass]="quorumReached(option) ? 'green-text' : 'red-warning-text'"
|
||||||
<mat-icon>done</mat-icon>
|
matTooltip="{{ getQuorumReachedString(option) }}"
|
||||||
</span>
|
>
|
||||||
<span *ngIf="!quorumReached(option)" class="red-warning-text">
|
<mat-icon *ngIf="quorumReached(option)">{{ pollService.getIcon('yes') }}</mat-icon>
|
||||||
<mat-icon>cancel</mat-icon>
|
<mat-icon *ngIf="!quorumReached(option)">{{ pollService.getIcon('no') }}</mat-icon>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
<!-- summary -->
|
<!-- summary -->
|
||||||
<div *ngFor="let key of pollValues">
|
|
||||||
<div>
|
<div>
|
||||||
<span>{{ key | translate }}</span>:
|
<div *ngFor="let key of pollValues" class="poll-grid">
|
||||||
|
<div></div>
|
||||||
|
<div class="candidate-name">
|
||||||
|
<span>{{ pollService.getLabel(key) | translate }}</span
|
||||||
|
>:
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{ poll[key] | precisionPipe }}
|
{{ pollService.getSpecialLabel(poll[key]) | translate }}
|
||||||
{{ pollService.getSpecialLabel(poll[key]) }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div *ngIf="!pollData">
|
||||||
|
<h4 translate>Candidates</h4>
|
||||||
|
<div *ngFor="let option of poll.options">
|
||||||
|
<span class="accent"> {{ option.user.full_name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
@ -59,3 +59,36 @@
|
|||||||
padding: 1px;
|
padding: 1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.poll-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
grid-template-columns: 30px auto 250px 150px;
|
||||||
|
.candidate-name {
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.poll-border {
|
||||||
|
border: 1px solid lightgray;
|
||||||
|
}
|
||||||
|
.poll-menu {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.poll-quorum {
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 10px;
|
||||||
|
mat-icon {
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.top-aligned {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wide {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Component, OnInit, Input } from '@angular/core';
|
import { Component, OnInit, Input } from '@angular/core';
|
||||||
|
import { FormGroup, FormBuilder } from '@angular/forms';
|
||||||
import { MatDialog, MatSnackBar } from '@angular/material';
|
import { MatDialog, MatSnackBar } from '@angular/material';
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
@ -6,15 +7,15 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
import { AssignmentPollDialogComponent } from '../assignment-poll-dialog/assignment-poll-dialog.component';
|
import { AssignmentPollDialogComponent } from '../assignment-poll-dialog/assignment-poll-dialog.component';
|
||||||
import { AssignmentPollService } from '../../services/assignment-poll.service';
|
import { AssignmentPollService } from '../../services/assignment-poll.service';
|
||||||
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
|
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
|
||||||
|
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||||
import { MajorityMethod, CalculablePollKey } from 'app/core/ui-services/poll.service';
|
import { MajorityMethod, CalculablePollKey } from 'app/core/ui-services/poll.service';
|
||||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||||
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
||||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
import { ViewAssignment } from '../../models/view-assignment';
|
|
||||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { ViewAssignmentPollOption } from '../../models/view-assignment-poll-option';
|
import { ViewAssignment } from '../../models/view-assignment';
|
||||||
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||||
|
import { ViewAssignmentPollOption } from '../../models/view-assignment-poll-option';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for a single assignment poll. Used in assignment detail view
|
* Component for a single assignment poll. Used in assignment detail view
|
||||||
@ -37,6 +38,11 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
@Input()
|
@Input()
|
||||||
public poll: ViewAssignmentPoll;
|
public poll: ViewAssignmentPoll;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form for updating the poll's description
|
||||||
|
*/
|
||||||
|
public descriptionForm: FormGroup;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The selected Majority method to display quorum calculations. Will be
|
* The selected Majority method to display quorum calculations. Will be
|
||||||
* set/changed by the user
|
* set/changed by the user
|
||||||
@ -63,6 +69,23 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
return this.pollService.pollValues.filter(name => this.poll[name] !== undefined);
|
return this.pollService.pollValues.filter(name => this.poll[name] !== undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns true if the description on the form differs from the poll's description
|
||||||
|
*/
|
||||||
|
public get dirtyDescription(): boolean {
|
||||||
|
return this.descriptionForm.get('description').value !== this.poll.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns true if vote results can be seen by the user
|
||||||
|
*/
|
||||||
|
public get pollData(): boolean {
|
||||||
|
if (!this.poll.has_votes) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.poll.published || this.canManage;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the translated poll method name
|
* Gets the translated poll method name
|
||||||
*
|
*
|
||||||
@ -90,6 +113,8 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
/**
|
/**
|
||||||
* constructor. Does nothing
|
* constructor. Does nothing
|
||||||
*
|
*
|
||||||
|
* @param titleService
|
||||||
|
* @param matSnackBar
|
||||||
* @param pollService poll related calculations
|
* @param pollService poll related calculations
|
||||||
* @param operator permission checks
|
* @param operator permission checks
|
||||||
* @param assignmentRepo The repository to the assignments
|
* @param assignmentRepo The repository to the assignments
|
||||||
@ -105,7 +130,8 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
private assignmentRepo: AssignmentRepositoryService,
|
private assignmentRepo: AssignmentRepositoryService,
|
||||||
public translate: TranslateService,
|
public translate: TranslateService,
|
||||||
public dialog: MatDialog,
|
public dialog: MatDialog,
|
||||||
private promptService: PromptService
|
private promptService: PromptService,
|
||||||
|
private formBuilder: FormBuilder
|
||||||
) {
|
) {
|
||||||
super(titleService, translate, matSnackBar);
|
super(titleService, translate, matSnackBar);
|
||||||
}
|
}
|
||||||
@ -117,6 +143,9 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
this.majorityChoice =
|
this.majorityChoice =
|
||||||
this.pollService.majorityMethods.find(method => method.value === this.pollService.defaultMajorityMethod) ||
|
this.pollService.majorityMethods.find(method => method.value === this.pollService.defaultMajorityMethod) ||
|
||||||
null;
|
null;
|
||||||
|
this.descriptionForm = this.formBuilder.group({
|
||||||
|
description: this.poll ? this.poll.description : ''
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -159,14 +188,9 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
* closes successfully (validation is done there)
|
* closes successfully (validation is done there)
|
||||||
*/
|
*/
|
||||||
public enterVotes(): void {
|
public enterVotes(): void {
|
||||||
// TODO deep copy of this.poll (JSON parse is ugly workaround)
|
|
||||||
// or sending just copy of the options
|
|
||||||
const data = {
|
|
||||||
poll: JSON.parse(JSON.stringify(this.poll)),
|
|
||||||
users: this.assignment.candidates // used to get the names of the users
|
|
||||||
};
|
|
||||||
const dialogRef = this.dialog.open(AssignmentPollDialogComponent, {
|
const dialogRef = this.dialog.open(AssignmentPollDialogComponent, {
|
||||||
data: data,
|
// TODO deep copy of this.poll (JSON parse is ugly workaround) or sending just copy of the options
|
||||||
|
data: this.poll.copy(),
|
||||||
maxHeight: '90vh',
|
maxHeight: '90vh',
|
||||||
minWidth: '300px',
|
minWidth: '300px',
|
||||||
maxWidth: '80vw',
|
maxWidth: '80vw',
|
||||||
@ -213,4 +237,27 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
|
|||||||
this.assignmentRepo.markElected(viewAssignmentRelatedUser, this.assignment, !option.is_elected);
|
this.assignmentRepo.markElected(viewAssignmentRelatedUser, this.assignment, !option.is_elected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the edited poll description to the server
|
||||||
|
* TODO: Better feedback
|
||||||
|
*/
|
||||||
|
public async onEditDescriptionButton(): Promise<void> {
|
||||||
|
const desc: string = this.descriptionForm.get('description').value;
|
||||||
|
await this.assignmentRepo.updatePoll({ description: desc }, this.poll).then(null, this.raiseError);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a tooltip string about the quorum
|
||||||
|
* @param option
|
||||||
|
* @returns a translated
|
||||||
|
*/
|
||||||
|
public getQuorumReachedString(option: ViewAssignmentPollOption): string {
|
||||||
|
const name = this.translate.instant(this.majorityChoice.display_name);
|
||||||
|
const quorum = this.pollService.yesQuorum(this.majorityChoice, this.poll, option);
|
||||||
|
const isReached = this.quorumReached(option)
|
||||||
|
? this.translate.instant('reached')
|
||||||
|
: this.translate.instant('not reached');
|
||||||
|
return `${name} (${quorum}) ${isReached}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import { Identifiable } from 'app/shared/models/base/identifiable';
|
|||||||
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
||||||
import { AssignmentPollMethod } from '../services/assignment-poll.service';
|
import { AssignmentPollMethod } from '../services/assignment-poll.service';
|
||||||
import { ViewAssignmentPollOption } from './view-assignment-poll-option';
|
import { ViewAssignmentPollOption } from './view-assignment-poll-option';
|
||||||
|
import { AssignmentPollOption } from 'app/shared/models/assignments/assignment-poll-option';
|
||||||
|
|
||||||
export class ViewAssignmentPoll implements Identifiable, Updateable {
|
export class ViewAssignmentPoll implements Identifiable, Updateable {
|
||||||
private _assignmentPoll: AssignmentPoll;
|
private _assignmentPoll: AssignmentPoll;
|
||||||
@ -37,14 +38,25 @@ export class ViewAssignmentPoll implements Identifiable, Updateable {
|
|||||||
return this.poll.votesvalid;
|
return this.poll.votesvalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public set votesvalid(amount: number) {
|
||||||
|
this.poll.votesvalid = amount;
|
||||||
|
}
|
||||||
|
|
||||||
public get votesinvalid(): number {
|
public get votesinvalid(): number {
|
||||||
return this.poll.votesinvalid;
|
return this.poll.votesinvalid;
|
||||||
}
|
}
|
||||||
|
public set votesinvalid(amount: number) {
|
||||||
|
this.poll.votesinvalid = amount;
|
||||||
|
}
|
||||||
|
|
||||||
public get votescast(): number {
|
public get votescast(): number {
|
||||||
return this.poll.votescast;
|
return this.poll.votescast;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public set votescast(amount: number) {
|
||||||
|
this.poll.votescast = amount;
|
||||||
|
}
|
||||||
|
|
||||||
public get has_votes(): boolean {
|
public get has_votes(): boolean {
|
||||||
return this.poll.has_votes;
|
return this.poll.has_votes;
|
||||||
}
|
}
|
||||||
@ -68,4 +80,22 @@ export class ViewAssignmentPoll implements Identifiable, Updateable {
|
|||||||
public updateDependencies(update: BaseViewModel): void {
|
public updateDependencies(update: BaseViewModel): void {
|
||||||
this.options.forEach(option => option.updateDependencies(update));
|
this.options.forEach(option => option.updateDependencies(update));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a copy with deep-copy on all changing numerical values,
|
||||||
|
* but intact uncopied references to the users
|
||||||
|
*
|
||||||
|
* TODO check and review
|
||||||
|
*/
|
||||||
|
public copy(): ViewAssignmentPoll {
|
||||||
|
return new ViewAssignmentPoll(
|
||||||
|
new AssignmentPoll(JSON.parse(JSON.stringify(this._assignmentPoll))),
|
||||||
|
this._assignmentPollOptions.map(option => {
|
||||||
|
return new ViewAssignmentPollOption(
|
||||||
|
new AssignmentPollOption(JSON.parse(JSON.stringify(option.option))),
|
||||||
|
option.user
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { AssignmentRelatedUser } from 'app/shared/models/assignments/assignment-related-user';
|
import { AssignmentRelatedUser } from 'app/shared/models/assignments/assignment-related-user';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
|
||||||
import { BaseViewModel } from 'app/site/base/base-view-model';
|
import { BaseViewModel } from 'app/site/base/base-view-model';
|
||||||
import { Updateable } from 'app/site/base/updateable';
|
import { Displayable } from 'app/site/base/displayable';
|
||||||
import { Identifiable } from 'app/shared/models/base/identifiable';
|
import { Identifiable } from 'app/shared/models/base/identifiable';
|
||||||
|
import { Updateable } from 'app/site/base/updateable';
|
||||||
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
|
|
||||||
export class ViewAssignmentRelatedUser implements Updateable, Identifiable {
|
export class ViewAssignmentRelatedUser implements Updateable, Identifiable, Displayable {
|
||||||
private _assignmentRelatedUser: AssignmentRelatedUser;
|
private _assignmentRelatedUser: AssignmentRelatedUser;
|
||||||
private _user?: ViewUser;
|
private _user?: ViewUser;
|
||||||
|
|
||||||
@ -46,4 +47,12 @@ export class ViewAssignmentRelatedUser implements Updateable, Identifiable {
|
|||||||
this._user = update;
|
this._user = update;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getTitle(): string {
|
||||||
|
return this.user ? this.user.getTitle() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public getListTitle(): string {
|
||||||
|
return this.getTitle();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,28 @@ import { BaseViewModel } from 'app/site/base/base-view-model';
|
|||||||
import { ViewAssignmentRelatedUser } from './view-assignment-related-user';
|
import { ViewAssignmentRelatedUser } from './view-assignment-related-user';
|
||||||
import { ViewAssignmentPoll } from './view-assignment-poll';
|
import { ViewAssignmentPoll } from './view-assignment-poll';
|
||||||
|
|
||||||
export interface AssignmentPhase {
|
/**
|
||||||
value: number;
|
* A constant containing all possible assignment phases and their different
|
||||||
display_name: string;
|
* representations as numerical value, string as used in server, and the display
|
||||||
}
|
* name.
|
||||||
|
*/
|
||||||
|
export const AssignmentPhases: { name: string; value: number; display_name: string }[] = [
|
||||||
|
{
|
||||||
|
name: 'PHASE_SEARCH',
|
||||||
|
value: 0,
|
||||||
|
display_name: 'Searching for candidates'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'PHASE_VOTING',
|
||||||
|
value: 1,
|
||||||
|
display_name: 'Voting'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'PHASE_FINISHED',
|
||||||
|
value: 2,
|
||||||
|
display_name: 'Finished'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
export class ViewAssignment extends BaseAgendaViewModel {
|
export class ViewAssignment extends BaseAgendaViewModel {
|
||||||
public static COLLECTIONSTRING = Assignment.COLLECTIONSTRING;
|
public static COLLECTIONSTRING = Assignment.COLLECTIONSTRING;
|
||||||
@ -62,12 +80,37 @@ export class ViewAssignment extends BaseAgendaViewModel {
|
|||||||
return this.assignment.phase;
|
return this.assignment.phase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get phaseString(): string {
|
||||||
|
const phase = AssignmentPhases.find(ap => ap.value === this.assignment.phase);
|
||||||
|
return phase ? phase.display_name : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns true if the assignment is in the 'finished' state
|
||||||
|
* (not accepting votes or candidates anymore)
|
||||||
|
*/
|
||||||
|
public get isFinished(): boolean {
|
||||||
|
const finishedState = AssignmentPhases.find(ap => ap.name === 'PHASE_FINISHED');
|
||||||
|
return this.phase === finishedState.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns true if the assignment is in the 'searching' state
|
||||||
|
*/
|
||||||
|
public get isSearchingForCandidates(): boolean {
|
||||||
|
const searchState = AssignmentPhases.find(ap => ap.name === 'PHASE_SEARCH');
|
||||||
|
return this.phase === searchState.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns the amount of candidates in the assignment's candidate list
|
||||||
|
*/
|
||||||
public get candidateAmount(): number {
|
public get candidateAmount(): number {
|
||||||
return this._assignmentRelatedUsers ? this._assignmentRelatedUsers.length : 0;
|
return this._assignmentRelatedUsers ? this._assignmentRelatedUsers.length : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is set by the repository
|
* Constructor. Is set by the repository
|
||||||
*/
|
*/
|
||||||
public getVerboseName;
|
public getVerboseName;
|
||||||
public getAgendaTitle;
|
public getAgendaTitle;
|
||||||
|
@ -4,8 +4,7 @@ import { AssignmentRepositoryService } from 'app/core/repositories/assignments/a
|
|||||||
import { Assignment } from 'app/shared/models/assignments/assignment';
|
import { Assignment } from 'app/shared/models/assignments/assignment';
|
||||||
import { BaseFilterListService, OsFilter } from 'app/core/ui-services/base-filter-list.service';
|
import { BaseFilterListService, OsFilter } from 'app/core/ui-services/base-filter-list.service';
|
||||||
import { StorageService } from 'app/core/core-services/storage.service';
|
import { StorageService } from 'app/core/core-services/storage.service';
|
||||||
import { ViewAssignment, AssignmentPhase } from '../models/view-assignment';
|
import { ViewAssignment, AssignmentPhases } from '../models/view-assignment';
|
||||||
import { ConstantsService } from 'app/core/core-services/constants.service';
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -37,11 +36,7 @@ export class AssignmentFilterListService extends BaseFilterListService<Assignmen
|
|||||||
* @param assignmentRepo Repository
|
* @param assignmentRepo Repository
|
||||||
* @param constants the openslides constant service to get the assignment options
|
* @param constants the openslides constant service to get the assignment options
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(store: StorageService, assignmentRepo: AssignmentRepositoryService) {
|
||||||
store: StorageService,
|
|
||||||
assignmentRepo: AssignmentRepositoryService,
|
|
||||||
private constants: ConstantsService
|
|
||||||
) {
|
|
||||||
super(store, assignmentRepo);
|
super(store, assignmentRepo);
|
||||||
this.createPhaseOptions();
|
this.createPhaseOptions();
|
||||||
}
|
}
|
||||||
@ -51,14 +46,8 @@ export class AssignmentFilterListService extends BaseFilterListService<Assignmen
|
|||||||
* constants
|
* constants
|
||||||
*/
|
*/
|
||||||
private createPhaseOptions(): void {
|
private createPhaseOptions(): void {
|
||||||
this.constants.get<AssignmentPhase[]>('AssignmentPhases').subscribe(phases => {
|
this.phaseFilter.options = AssignmentPhases.map(ap => {
|
||||||
this.phaseFilter.options = phases.map(ph => {
|
return { label: ap.display_name, condition: ap.value, isActive: false };
|
||||||
return {
|
|
||||||
label: ph.display_name,
|
|
||||||
condition: ph.value,
|
|
||||||
isActive: false
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
this.updateFilterDefinitions(this.filterOptions);
|
this.updateFilterDefinitions(this.filterOptions);
|
||||||
}
|
}
|
||||||
|
@ -117,6 +117,25 @@ export class AssignmentPollService extends PollService {
|
|||||||
return Math.round(((vote.weight * 100) / base) * 100) / 100;
|
return Math.round(((vote.weight * 100) / base) * 100) / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the percentage for a non-abstract per-poll value
|
||||||
|
* TODO: similar code to getPercent. Mergeable?
|
||||||
|
*
|
||||||
|
* @param poll the poll this value refers to
|
||||||
|
* @param value a per-poll value (e.g. 'votesvalid')
|
||||||
|
* @returns a percentage number with two digits, null if the value cannot be calculated
|
||||||
|
*/
|
||||||
|
public getValuePercent(poll: ViewAssignmentPoll, value: CalculablePollKey): number | null {
|
||||||
|
if (!poll.pollBase) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const amount = poll[value];
|
||||||
|
if (amount === undefined || amount < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Math.round(((amount * 100) / poll.pollBase) * 100) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the option in a poll is abstract (percentages should not be calculated)
|
* Check if the option in a poll is abstract (percentages should not be calculated)
|
||||||
*
|
*
|
||||||
|
@ -26,7 +26,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'assignments',
|
path: 'assignments',
|
||||||
loadChildren: './assignments/assignments.module#AssignmentsModule',
|
loadChildren: './assignments/assignments.module#AssignmentsModule',
|
||||||
data: { basePerm: 'assignment.can_see' }
|
data: { basePerm: 'assignments.can_see' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'mediafiles',
|
path: 'mediafiles',
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
from typing import Any, Dict, List, Set
|
from typing import Any, Dict, Set
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from mypy_extensions import TypedDict
|
|
||||||
|
|
||||||
|
|
||||||
class AssignmentsAppConfig(AppConfig):
|
class AssignmentsAppConfig(AppConfig):
|
||||||
@ -51,14 +50,6 @@ class AssignmentsAppConfig(AppConfig):
|
|||||||
"""
|
"""
|
||||||
yield self.get_model("Assignment")
|
yield self.get_model("Assignment")
|
||||||
|
|
||||||
def get_angular_constants(self):
|
|
||||||
assignment = self.get_model("Assignment")
|
|
||||||
Item = TypedDict("Item", {"value": int, "display_name": str})
|
|
||||||
phases: List[Item] = []
|
|
||||||
for phase in assignment.PHASES:
|
|
||||||
phases.append({"value": phase[0], "display_name": phase[1]})
|
|
||||||
return {"AssignmentPhases": phases}
|
|
||||||
|
|
||||||
|
|
||||||
def required_users(element: Dict[str, Any]) -> Set[int]:
|
def required_users(element: Dict[str, Any]) -> Set[int]:
|
||||||
"""
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user