Show latest meaningfull poll results in autopilot

Shows the latest meaningfull poll result in autopilot
The last published poll result from the corresponding content object
This commit is contained in:
Sean 2020-11-16 17:23:39 +01:00
parent 2943c969ab
commit 10614ca57b
20 changed files with 373 additions and 232 deletions

View File

@ -1,5 +1,5 @@
<div *ngIf="poll"> <div *ngIf="poll">
<table class="assignment-result-table"> <table class="assignment-result-table" *ngIf="hasResults && canSeeResults">
<tbody> <tbody>
<tr> <tr>
<th class="voting-option">{{ 'Candidates' | translate }}</th> <th class="voting-option">{{ 'Candidates' | translate }}</th>
@ -51,4 +51,18 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<!-- No results yet -->
<div *ngIf="!hasResults">
<i>
{{ 'No results yet.' | translate }}
</i>
</div>
<!-- Has results, but user cannot see -->
<div *ngIf="hasResults && !canSeeResults">
<i>
{{ 'Counting of votes is in progress ...' | translate }}
</i>
</div>
</div> </div>

View File

@ -1,6 +1,12 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { BaseComponent } from 'app/base.component';
import { OperatorService } from 'app/core/core-services/operator.service';
import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll'; import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
import { PollState } from 'app/shared/models/poll/base-poll';
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll'; import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
import { AssignmentPollService } from 'app/site/assignments/modules/assignment-poll/services/assignment-poll.service'; import { AssignmentPollService } from 'app/site/assignments/modules/assignment-poll/services/assignment-poll.service';
import { PollData, PollTableData, VotingResult } from 'app/site/polls/services/poll.service'; import { PollData, PollTableData, VotingResult } from 'app/site/polls/services/poll.service';
@ -10,16 +16,18 @@ import { PollData, PollTableData, VotingResult } from 'app/site/polls/services/p
templateUrl: './assignment-poll-detail-content.component.html', templateUrl: './assignment-poll-detail-content.component.html',
styleUrls: ['./assignment-poll-detail-content.component.scss'] styleUrls: ['./assignment-poll-detail-content.component.scss']
}) })
export class AssignmentPollDetailContentComponent { export class AssignmentPollDetailContentComponent extends BaseComponent {
@Input() @Input()
public poll: ViewAssignmentPoll | PollData; public poll: ViewAssignmentPoll | PollData;
public constructor(private pollService: AssignmentPollService) {}
private get method(): string { private get method(): string {
return this.poll.pollmethod; return this.poll.pollmethod;
} }
private get state(): PollState {
return this.poll.state;
}
public get showYHeader(): boolean { public get showYHeader(): boolean {
return this.isMethodY || this.isMethodYN || this.isMethodYNA; return this.isMethodY || this.isMethodYN || this.isMethodYNA;
} }
@ -44,10 +52,35 @@ export class AssignmentPollDetailContentComponent {
return this.method === AssignmentPollMethod.YNA; return this.method === AssignmentPollMethod.YNA;
} }
public get isFinished(): boolean {
return this.state === PollState.Finished;
}
public get isPublished(): boolean {
return this.state === PollState.Published;
}
public get tableData(): PollTableData[] { public get tableData(): PollTableData[] {
return this.pollService.generateTableData(this.poll); return this.pollService.generateTableData(this.poll);
} }
public get hasResults(): boolean {
return this.isFinished || this.isPublished;
}
public get canSeeResults(): boolean {
return this.operator.hasPerms(this.permission.assignmentsCanManage) || this.isPublished;
}
public constructor(
titleService: Title,
translateService: TranslateService,
private pollService: AssignmentPollService,
private operator: OperatorService
) {
super(titleService, translateService);
}
public getVoteClass(votingResult: VotingResult): string { public getVoteClass(votingResult: VotingResult): string {
const votingClass = votingResult.vote; const votingClass = votingResult.vote;
if (this.isMethodN && votingClass === 'no') { if (this.isMethodN && votingClass === 'no') {

View File

@ -74,7 +74,9 @@ export class ChartsComponent extends BaseViewComponentDirective {
@Input() @Input()
public set data(inputData: ChartData) { public set data(inputData: ChartData) {
this.progressInputData(inputData); if (inputData) {
this.progressInputData(inputData);
}
} }
/** /**
@ -85,6 +87,9 @@ export class ChartsComponent extends BaseViewComponentDirective {
return { return {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
animation: {
duration: 0
},
tooltips: { tooltips: {
enabled: false enabled: false
}, },
@ -96,6 +101,9 @@ export class ChartsComponent extends BaseViewComponentDirective {
return { return {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
animation: {
duration: 0
},
tooltips: { tooltips: {
enabled: false enabled: false
}, },

View File

@ -1,39 +1,55 @@
<div class="result-wrapper" *ngIf="hasVotes"> <div class="result-wrapper" *ngIf="poll">
<!-- result table --> <ng-container *ngIf="hasResults && canSeeResults">
<table class="result-table"> <!-- result table -->
<tbody> <table class="result-table">
<tr> <tbody>
<th></th> <tr>
<th colspan="2">{{ 'Votes' | translate }}</th> <th></th>
</tr> <th colspan="2">{{ 'Votes' | translate }}</th>
<tr *ngFor="let row of getTableData()" [class]="row.votingOption"> </tr>
<!-- YNA/Valid etc --> <tr *ngFor="let row of tableData; trackBy: trackByIndex" [class]="row.votingOption">
<td> <!-- YNA/Valid etc -->
<os-icon-container *ngIf="row.value[0].icon" [icon]="row.value[0].icon" [size]="iconSize"> <td>
{{ row.votingOption | pollKeyVerbose | translate }} <os-icon-container *ngIf="row.value[0].icon" [icon]="row.value[0].icon" [size]="iconSize">
</os-icon-container> {{ row.votingOption | pollKeyVerbose | translate }}
<span *ngIf="!row.value[0].icon"> </os-icon-container>
{{ row.votingOption | pollKeyVerbose | translate }} <span *ngIf="!row.value[0].icon">
</span> {{ row.votingOption | pollKeyVerbose | translate }}
</td> </span>
</td>
<!-- Percent numbers --> <!-- Percent numbers -->
<td class="result-cell-definition"> <td class="result-cell-definition">
<span *ngIf="row.value[0].showPercent"> <span *ngIf="row.value[0].showPercent">
{{ row.value[0].amount | pollPercentBase: poll:'motion' }} {{ row.value[0].amount | pollPercentBase: poll:'motion' }}
</span> </span>
</td> </td>
<!-- Voices --> <!-- Voices -->
<td class="result-cell-definition"> <td class="result-cell-definition">
{{ row.value[0].amount | parsePollNumber }} {{ row.value[0].amount | parsePollNumber }}
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<!-- Chart --> <!-- Chart -->
<div class="doughnut-chart" *ngIf="showChart"> <div class="doughnut-chart">
<os-charts type="doughnut" [data]="chartData | async"></os-charts> <os-charts type="doughnut" [data]="chartData"></os-charts>
</div>
</ng-container>
<!-- No results yet -->
<div *ngIf="!hasResults">
<i>
{{ 'No results yet.' | translate }}
</i>
</div>
<!-- Has results, but user cannot see -->
<div *ngIf="hasResults && !canSeeResults">
<i>
{{ 'Counting of votes is in progress ...' | translate }}
</i>
</div> </div>
</div> </div>

View File

@ -1,7 +1,11 @@
import { Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { BehaviorSubject } from 'rxjs'; import { TranslateService } from '@ngx-translate/core';
import { BaseComponent } from 'app/base.component';
import { OperatorService } from 'app/core/core-services/operator.service';
import { PollState } from 'app/shared/models/poll/base-poll';
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll'; import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
import { MotionPollService } from 'app/site/motions/services/motion-poll.service'; import { MotionPollService } from 'app/site/motions/services/motion-poll.service';
import { PollData, PollTableData } from 'app/site/polls/services/poll.service'; import { PollData, PollTableData } from 'app/site/polls/services/poll.service';
@ -10,31 +14,65 @@ import { ChartData } from '../charts/charts.component';
@Component({ @Component({
selector: 'os-motion-poll-detail-content', selector: 'os-motion-poll-detail-content',
templateUrl: './motion-poll-detail-content.component.html', templateUrl: './motion-poll-detail-content.component.html',
styleUrls: ['./motion-poll-detail-content.component.scss'] styleUrls: ['./motion-poll-detail-content.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class MotionPollDetailContentComponent implements OnInit { export class MotionPollDetailContentComponent extends BaseComponent {
@Input() private _poll: ViewMotionPoll | PollData;
public poll: ViewMotionPoll | PollData;
public chartData: ChartData;
public tableData: PollTableData[];
@Input() @Input()
public chartData: BehaviorSubject<ChartData>; public set poll(pollData: ViewMotionPoll | PollData) {
this._poll = pollData;
this.setTableData();
this.setChartData();
this.cd.markForCheck();
}
public get poll(): ViewMotionPoll | PollData {
return this._poll;
}
@Input() @Input()
public iconSize: 'large' | 'gigantic' = 'large'; public iconSize: 'large' | 'gigantic' = 'large';
public get hasVotes(): boolean { private get state(): PollState {
return this.poll && !!this.poll.options; return this.poll.state;
} }
public constructor(private motionPollService: MotionPollService) {} public get hasResults(): boolean {
return this.isFinished || this.isPublished;
public ngOnInit(): void {}
public getTableData(): PollTableData[] {
return this.motionPollService.generateTableData(this.poll);
} }
public get showChart(): boolean { public get isFinished(): boolean {
return this.motionPollService.showChart(this.poll) && this.chartData && !!this.chartData.value; return this.state === PollState.Finished;
}
public get isPublished(): boolean {
return this.state === PollState.Published;
}
public get canSeeResults(): boolean {
return this.operator.hasPerms(this.permission.motionsCanManagePolls) || this.isPublished;
}
public constructor(
titleService: Title,
translate: TranslateService,
private pollService: MotionPollService,
private cd: ChangeDetectorRef,
private operator: OperatorService
) {
super(titleService, translate);
}
private setTableData(): void {
this.tableData = this.pollService.generateTableData(this.poll);
}
private setChartData(): void {
this.chartData = this.pollService.generateChartData(this.poll);
} }
} }

View File

@ -28,22 +28,17 @@
</span> </span>
</div> </div>
<div class="assignment-result-wrapper" *ngIf="poll && poll.stateHasVotes"> <div class="assignment-result-wrapper" *ngIf="poll">
<!-- Result Table --> <!-- Result Table -->
<os-assignment-poll-detail-content [poll]="poll"></os-assignment-poll-detail-content> <os-assignment-poll-detail-content [poll]="poll"></os-assignment-poll-detail-content>
<!-- Result Chart --> <!-- Result Chart -->
<div class="chart-wrapper"> <div class="chart-wrapper" *ngIf="showResults && poll.stateHasVotes">
<os-charts <os-charts class="assignment-result-chart" [labels]="candidatesLabels" [data]="chartData"></os-charts>
class="assignment-result-chart"
*ngIf="chartDataSubject.value"
[labels]="candidatesLabels"
[data]="chartDataSubject | async"
></os-charts>
</div> </div>
<!-- Single Votes Table --> <!-- Single Votes Table -->
<div class="named-result-table" *ngIf="poll.type === 'named'"> <div class="named-result-table" *ngIf="showResults && poll.stateHasVotes && poll.type === 'named'">
<h3>{{ 'Single votes' | translate }}</h3> <h3>{{ 'Single votes' | translate }}</h3>
<os-list-view-table <os-list-view-table
class="single-votes-table" class="single-votes-table"

View File

@ -1,4 +1,4 @@
import { Component, ViewEncapsulation } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ViewEncapsulation } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
@ -12,6 +12,7 @@ import { AssignmentVoteRepositoryService } from 'app/core/repositories/assignmen
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service'; import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
import { ChartData } from 'app/shared/components/charts/charts.component';
import { VoteValue } from 'app/shared/models/poll/base-vote'; import { VoteValue } from 'app/shared/models/poll/base-vote';
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll'; import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
import { BasePollDetailComponentDirective } from 'app/site/polls/components/base-poll-detail.component'; import { BasePollDetailComponentDirective } from 'app/site/polls/components/base-poll-detail.component';
@ -22,6 +23,7 @@ import { AssignmentPollService } from '../../services/assignment-poll.service';
selector: 'os-assignment-poll-detail', selector: 'os-assignment-poll-detail',
templateUrl: './assignment-poll-detail.component.html', templateUrl: './assignment-poll-detail.component.html',
styleUrls: ['./assignment-poll-detail.component.scss'], styleUrls: ['./assignment-poll-detail.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class AssignmentPollDetailComponent extends BasePollDetailComponentDirective< export class AssignmentPollDetailComponent extends BasePollDetailComponentDirective<
@ -38,6 +40,14 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponentDirect
public isVoteWeightActive: boolean; public isVoteWeightActive: boolean;
public get showResults(): boolean {
return this.hasPerms() || this.poll.isPublished;
}
public get chartData(): ChartData {
return this.pollService.generateChartData(this.poll);
}
public constructor( public constructor(
title: Title, title: Title,
translate: TranslateService, translate: TranslateService,
@ -51,7 +61,8 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponentDirect
protected pollService: AssignmentPollService, protected pollService: AssignmentPollService,
votesRepo: AssignmentVoteRepositoryService, votesRepo: AssignmentVoteRepositoryService,
protected operator: OperatorService, protected operator: OperatorService,
private router: Router private router: Router,
protected cd: ChangeDetectorRef
) { ) {
super( super(
title, title,
@ -64,7 +75,8 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponentDirect
pollDialog, pollDialog,
pollService, pollService,
votesRepo, votesRepo,
operator operator,
cd
); );
configService configService
.get<boolean>('users_activate_vote_weight') .get<boolean>('users_activate_vote_weight')

View File

@ -12,9 +12,7 @@
<div class="italic spacer-bottom-20"> <div class="italic spacer-bottom-20">
<span *osPerms="'assignments.can_manage'; and: poll.type === 'pseudoanonymous'"> <span *osPerms="'assignments.can_manage'; and: poll.type === 'pseudoanonymous'">
<button mat-icon-button color="warn" (click)="openVotingWarning()"> <button mat-icon-button color="warn" (click)="openVotingWarning()">
<mat-icon> <mat-icon> warning </mat-icon>
warning
</mat-icon>
</button> </button>
</span> </span>
<span *ngIf="poll.type !== 'analog'"> {{ poll.typeVerbose | translate }} &middot; </span> <span *ngIf="poll.type !== 'analog'"> {{ poll.typeVerbose | translate }} &middot; </span>
@ -64,19 +62,8 @@
<!-- Chart / Table --> <!-- Chart / Table -->
<div *ngIf="poll.stateHasVotes" class="poll-result-wrapper"> <div *ngIf="poll.stateHasVotes" class="poll-result-wrapper">
<div *osPerms="'assignments.can_manage'; or: poll.isPublished"> <os-assignment-poll-detail-content routerLink="/assignments/polls/{{ poll.id }}" [poll]="poll">
<os-assignment-poll-detail-content </os-assignment-poll-detail-content>
routerLink="/assignments/polls/{{ poll.id }}"
[poll]="poll"
></os-assignment-poll-detail-content>
</div>
<!-- Cannot see unpublished -->
<div *osPerms="'assignments.can_manage'; complement: true">
<span *ngIf="poll.isFinished">
{{ 'Counting of votes is in progress ...' | translate }}
</span>
</div>
</div> </div>
<!-- Poll progress bar --> <!-- Poll progress bar -->
@ -95,9 +82,7 @@
matTooltip="{{ 'More' | translate }}" matTooltip="{{ 'More' | translate }}"
*ngIf="poll.isPublished" *ngIf="poll.isPublished"
> >
<mat-icon class="small-icon"> <mat-icon class="small-icon"> visibility </mat-icon>
visibility
</mat-icon>
</a> </a>
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
@ -90,7 +90,8 @@ export class CinemaComponent extends BaseViewComponentDirective implements OnIni
private projectorService: ProjectorService, private projectorService: ProjectorService,
private projectorRepo: ProjectorRepositoryService, private projectorRepo: ProjectorRepositoryService,
private closService: CurrentListOfSpeakersService, private closService: CurrentListOfSpeakersService,
private listOfSpeakersRepo: ListOfSpeakersRepositoryService private listOfSpeakersRepo: ListOfSpeakersRepositoryService,
private cd: ChangeDetectorRef
) { ) {
super(title, translate, snackBar); super(title, translate, snackBar);
} }

View File

@ -1,13 +1,32 @@
<mat-card class="os-card" *ngFor="let poll of polls; trackBy: identifyPoll"> <ng-container
<p class="subtitle-text"> *ngFor="let poll of polls; trackBy: identifyPoll"
<a [routerLink]="getPollDetailLink(poll)" [state]="{ back: 'true' }">{{ getPollVoteTitle(poll) }}</a> [ngTemplateOutlet]="pollArea"
</p> [ngTemplateOutletContext]="{ poll: poll, last: false }"
></ng-container>
<div *ngIf="poll.pollClassType === 'motion'"> <ng-container
<os-motion-poll-vote [poll]="poll" *ngIf="poll.canBeVotedFor()"></os-motion-poll-vote> *ngIf="lastPublishedPoll && !hasProjectedModelOpenPolls"
</div> [ngTemplateOutlet]="pollArea"
[ngTemplateOutletContext]="{ poll: lastPublishedPoll, last: true }"
></ng-container>
<div *ngIf="poll.pollClassType === 'assignment'"> <ng-template #pollArea let-poll="poll" let-last="last">
<os-assignment-poll-vote [poll]="poll" *ngIf="poll.canBeVotedFor()"></os-assignment-poll-vote> <mat-card class="os-card">
</div> <p class="subtitle-text">
</mat-card> <a [routerLink]="getPollDetailLink(poll)" [state]="{ back: 'true' }">{{ getPollVoteTitle(poll) }}</a>
</p>
<div *ngIf="poll.pollClassType === 'motion'">
<os-motion-poll-vote [poll]="poll" *ngIf="poll.canBeVotedFor() && !last"></os-motion-poll-vote>
<os-motion-poll-detail-content [poll]="lastPublishedPoll" *ngIf="last"></os-motion-poll-detail-content>
</div>
<div *ngIf="poll.pollClassType === 'assignment'">
<os-assignment-poll-vote [poll]="poll" *ngIf="poll.canBeVotedFor() && !last"></os-assignment-poll-vote>
<os-assignment-poll-detail-content
[poll]="lastPublishedPoll"
*ngIf="last"
></os-assignment-poll-detail-content>
</div>
</mat-card>
</ng-template>

View File

@ -1,25 +1,52 @@
import { Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { ViewAssignment } from 'app/site/assignments/models/view-assignment';
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
import { BaseViewComponentDirective } from 'app/site/base/base-view'; import { BaseViewComponentDirective } from 'app/site/base/base-view';
import { BaseViewModel } from 'app/site/base/base-view-model'; import { BaseViewModel } from 'app/site/base/base-view-model';
import { ViewMotion } from 'app/site/motions/models/view-motion';
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
import { ViewBasePoll } from 'app/site/polls/models/view-base-poll'; import { ViewBasePoll } from 'app/site/polls/models/view-base-poll';
import { PollListObservableService } from 'app/site/polls/services/poll-list-observable.service'; import { PollListObservableService } from 'app/site/polls/services/poll-list-observable.service';
@Component({ @Component({
selector: 'os-poll-collection', selector: 'os-poll-collection',
templateUrl: './poll-collection.component.html', templateUrl: './poll-collection.component.html',
styleUrls: ['./poll-collection.component.scss'] styleUrls: ['./poll-collection.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class PollCollectionComponent extends BaseViewComponentDirective implements OnInit { export class PollCollectionComponent extends BaseViewComponentDirective implements OnInit {
public polls: ViewBasePoll[]; public polls: ViewBasePoll[];
public lastPublishedPoll: ViewBasePoll;
private _currentProjection: BaseViewModel<any>;
public get currentProjection(): BaseViewModel<any> {
return this._currentProjection;
}
/**
* CLEANUP: This function belongs to "HasViewPolls"/ ViewModelWithPolls
*/
public get hasProjectedModelOpenPolls(): boolean {
if (this.currentProjection instanceof ViewMotion || this.currentProjection instanceof ViewAssignment) {
const currPolls: ViewMotionPoll[] | ViewAssignmentPoll[] = this.currentProjection.polls;
return currPolls.some((p: ViewMotionPoll | ViewAssignmentPoll) => p.isStarted);
}
return false;
}
@Input() @Input()
private currentProjection: BaseViewModel<any>; public set currentProjection(viewModel: BaseViewModel<any>) {
this._currentProjection = viewModel;
this.updateLastPublished();
}
private get showExtendedTitle(): boolean { private get showExtendedTitle(): boolean {
const areAllPollsSameModel = this.polls.every( const areAllPollsSameModel = this.polls.every(
@ -27,7 +54,7 @@ export class PollCollectionComponent extends BaseViewComponentDirective implemen
); );
if (this.currentProjection && areAllPollsSameModel) { if (this.currentProjection && areAllPollsSameModel) {
return this.polls[0].getContentObject() !== this.currentProjection; return this.polls[0]?.getContentObject() !== this.currentProjection;
} else { } else {
return !areAllPollsSameModel; return !areAllPollsSameModel;
} }
@ -37,7 +64,8 @@ export class PollCollectionComponent extends BaseViewComponentDirective implemen
title: Title, title: Title,
translate: TranslateService, translate: TranslateService,
snackBar: MatSnackBar, snackBar: MatSnackBar,
private pollService: PollListObservableService private pollService: PollListObservableService,
private cd: ChangeDetectorRef
) { ) {
super(title, translate, snackBar); super(title, translate, snackBar);
} }
@ -46,9 +74,17 @@ export class PollCollectionComponent extends BaseViewComponentDirective implemen
this.subscriptions.push( this.subscriptions.push(
this.pollService this.pollService
.getViewModelListObservable() .getViewModelListObservable()
.pipe(map(polls => polls.filter(poll => poll.canBeVotedFor()))) .pipe(
map(polls => {
return polls.filter(poll => {
return poll.canBeVotedFor();
});
})
)
.subscribe(polls => { .subscribe(polls => {
this.polls = polls; this.polls = polls;
this.cd.markForCheck();
this.updateLastPublished();
}) })
); );
} }
@ -57,6 +93,10 @@ export class PollCollectionComponent extends BaseViewComponentDirective implemen
return poll.id; return poll.id;
} }
public getPollDetailLink(poll: ViewBasePoll): string {
return poll.parentLink;
}
public getPollVoteTitle(poll: ViewBasePoll): string { public getPollVoteTitle(poll: ViewBasePoll): string {
const contentObject = poll.getContentObject(); const contentObject = poll.getContentObject();
const listTitle = contentObject.getListTitle(); const listTitle = contentObject.getListTitle();
@ -70,7 +110,35 @@ export class PollCollectionComponent extends BaseViewComponentDirective implemen
} }
} }
public getPollDetailLink(poll: ViewBasePoll): string { /**
return poll.parentLink; * Helper function to detect new latest published polls and set them.
*/
private updateLastPublished(): void {
const lastPublished = this.getLastfinshedPoll(this.currentProjection);
if (lastPublished !== this.lastPublishedPoll) {
this.lastPublishedPoll = lastPublished;
this.cd.markForCheck();
}
}
/**
* CLEANUP: This function belongs to "HasViewPolls"/ ViewModelWithPolls
* *class* (is an interface right now)
*
* @param viewModel
*/
private getLastfinshedPoll(viewModel: BaseViewModel): ViewBasePoll {
if (viewModel instanceof ViewMotion || viewModel instanceof ViewAssignment) {
let currPolls: ViewMotionPoll[] | ViewAssignmentPoll[] = viewModel.polls;
/**
* Although it should, since the union type could use `.filter
* without any problem, without an any cast it will not work
*/
currPolls = (currPolls as any[])
.filter((p: ViewMotionPoll | ViewAssignmentPoll) => p.stateHasVotes)
.reverse();
return currPolls[0];
}
return null;
} }
} }

View File

@ -11,6 +11,7 @@ import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { Searchable } from 'app/site/base/searchable'; import { Searchable } from 'app/site/base/searchable';
import { SlideOptions } from 'app/site/base/slide-options'; import { SlideOptions } from 'app/site/base/slide-options';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile'; import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { HasViewPolls } from 'app/site/polls/models/has-view-polls';
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 { AmendmentType } from '../motions.constants'; import { AmendmentType } from '../motions.constants';
@ -355,7 +356,7 @@ export class ViewMotion
} }
} }
interface TIMotionRelations { interface TIMotionRelations extends HasViewPolls<ViewMotionPoll> {
category?: ViewCategory; category?: ViewCategory;
submitters: ViewSubmitter[]; submitters: ViewSubmitter[];
supporters?: ViewUser[]; supporters?: ViewUser[];
@ -369,7 +370,6 @@ interface TIMotionRelations {
amendments?: ViewMotion[]; amendments?: ViewMotion[];
changeRecommendations?: ViewMotionChangeRecommendation[]; changeRecommendations?: ViewMotionChangeRecommendation[];
diffLines?: DiffLinesInParagraph[]; diffLines?: DiffLinesInParagraph[];
polls: ViewMotionPoll[];
} }
export interface ViewMotion extends MotionWithoutNestedModels, TIMotionRelations {} export interface ViewMotion extends MotionWithoutNestedModels, TIMotionRelations {}

View File

@ -29,68 +29,63 @@
</span> </span>
</div> </div>
<p *ngIf="!poll.hasVotes || !poll.stateHasVotes">{{ 'No results yet.' | translate }}</p> <os-motion-poll-detail-content [poll]="poll"></os-motion-poll-detail-content>
<div *ngIf="poll.stateHasVotes && (hasPerms() || poll.isPublished)"> <!-- Named table: only show if votes are present -->
<os-motion-poll-detail-content [poll]="poll" [chartData]="chartDataSubject"> <div class="named-result-table" *ngIf="showResults && poll.stateHasVotes && poll.type === 'named'">
</os-motion-poll-detail-content> <h2>{{ 'Single votes' | translate }}</h2>
<os-list-view-table
*ngIf="votesDataObservable"
class="single-votes-table"
[listObservable]="votesDataObservable"
[columns]="columnDefinition"
[filterProps]="filterProps"
[allowProjector]="false"
[fullScreen]="true"
[vScrollFixed]="-1"
listStorageKey="motion-poll-vote"
>
<!-- Header -->
<div *pblNgridHeaderCellDef="'*'; col as col">
{{ col.label | translate }}
</div>
<!-- Named table: only show if votes are present --> <!-- Content -->
<div class="named-result-table" *ngIf="poll.type === 'named'"> <div *pblNgridCellDef="'user'; row as vote">
<h2>{{ 'Single votes' | translate }}</h2> <div *ngIf="vote.user">
<os-list-view-table {{ vote.user.getShortName() }}
*ngIf="votesDataObservable"
class="single-votes-table"
[listObservable]="votesDataObservable"
[columns]="columnDefinition"
[filterProps]="filterProps"
[allowProjector]="false"
[fullScreen]="true"
[vScrollFixed]="-1"
listStorageKey="motion-poll-vote"
>
<!-- Header -->
<div *pblNgridHeaderCellDef="'*'; col as col">
{{ col.label | translate }}
</div>
<!-- Content --> <div class="user-subtitle">
<div *pblNgridCellDef="'user'; row as vote"> <!-- Level and number -->
<div *ngIf="vote.user"> <div *ngIf="vote.user.getLevelAndNumber()">
{{ vote.user.getShortName() }} {{ vote.user.getLevelAndNumber() }}
</div>
<div class="user-subtitle"> <!-- Vote weight -->
<!-- Level and number --> <div *ngIf="isVoteWeightActive">
<div *ngIf="vote.user.getLevelAndNumber()"> {{ 'Vote weight' | translate }}: {{ vote.user.vote_weight }}
{{ vote.user.getLevelAndNumber() }} </div>
</div>
<!-- Vote weight --> <!-- Delegation -->
<div *ngIf="isVoteWeightActive"> <div *ngIf="userHasVoteDelegation(vote.user)">
{{ 'Vote weight' | translate }}: {{ vote.user.vote_weight }} <span>
</div> ({{ 'represented by' | translate }}
{{ getUsersVoteDelegation(vote.user).getShortName().trim() }})
<!-- Delegation --> </span>
<div *ngIf="userHasVoteDelegation(vote.user)">
<span>
({{ 'represented by' | translate }}
{{ getUsersVoteDelegation(vote.user).getShortName().trim() }})
</span>
</div>
</div> </div>
</div> </div>
<div *ngIf="!vote.user">{{ 'Anonymous' | translate }}</div>
</div> </div>
<div *pblNgridCellDef="'vote'; row as vote" class="vote-cell"> <div *ngIf="!vote.user">{{ 'Anonymous' | translate }}</div>
<div class="vote-cell-icon-container" [ngClass]="voteOptionStyle[vote.value].css">
<mat-icon>{{ voteOptionStyle[vote.value].icon }}</mat-icon>
</div>
<div>{{ vote.valueVerbose | translate }}</div>
</div>
</os-list-view-table>
<div *ngIf="!votesDataObservable">
{{ 'The individual votes were anonymized.' | translate }}
</div> </div>
<div *pblNgridCellDef="'vote'; row as vote" class="vote-cell">
<div class="vote-cell-icon-container" [ngClass]="voteOptionStyle[vote.value].css">
<mat-icon>{{ voteOptionStyle[vote.value].icon }}</mat-icon>
</div>
<div>{{ vote.valueVerbose | translate }}</div>
</div>
</os-list-view-table>
<div *ngIf="!votesDataObservable">
{{ 'The individual votes were anonymized.' | translate }}
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ViewEncapsulation } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
@ -44,6 +44,10 @@ export class MotionPollDetailComponent extends BasePollDetailComponentDirective<
public isVoteWeightActive: boolean; public isVoteWeightActive: boolean;
public get showResults(): boolean {
return this.hasPerms() || this.poll.isPublished;
}
public constructor( public constructor(
title: Title, title: Title,
translate: TranslateService, translate: TranslateService,
@ -57,7 +61,8 @@ export class MotionPollDetailComponent extends BasePollDetailComponentDirective<
votesRepo: MotionVoteRepositoryService, votesRepo: MotionVoteRepositoryService,
configService: ConfigService, configService: ConfigService,
protected operator: OperatorService, protected operator: OperatorService,
private router: Router private router: Router,
protected cd: ChangeDetectorRef
) { ) {
super( super(
title, title,
@ -70,7 +75,8 @@ export class MotionPollDetailComponent extends BasePollDetailComponentDirective<
pollDialog, pollDialog,
pollService, pollService,
votesRepo, votesRepo,
operator operator,
cd
); );
configService configService
.get<boolean>('users_activate_vote_weight') .get<boolean>('users_activate_vote_weight')

View File

@ -23,9 +23,7 @@
<div class="italic spacer-bottom-20"> <div class="italic spacer-bottom-20">
<span *osPerms="'motions.can_manage_polls'; and: poll.type === 'pseudoanonymous'"> <span *osPerms="'motions.can_manage_polls'; and: poll.type === 'pseudoanonymous'">
<button mat-icon-button color="warn" (click)="openVotingWarning()"> <button mat-icon-button color="warn" (click)="openVotingWarning()">
<mat-icon> <mat-icon>warning</mat-icon>
warning
</mat-icon>
</button> </button>
</span> </span>
@ -42,7 +40,12 @@
<!-- Change state button --> <!-- Change state button -->
<div *osPerms="'motions.can_manage_polls'; and: !hideChangeState"> <div *osPerms="'motions.can_manage_polls'; and: !hideChangeState">
<button mat-stroked-button [ngClass]="pollStateActions[poll.state].css" (click)="changeState(poll.nextState)" [disabled]="stateChangePending"> <button
mat-stroked-button
[ngClass]="pollStateActions[poll.state].css"
(click)="changeState(poll.nextState)"
[disabled]="stateChangePending"
>
<mat-icon> {{ pollStateActions[poll.state].icon }}</mat-icon> <mat-icon> {{ pollStateActions[poll.state].icon }}</mat-icon>
<span class="next-state-label"> <span class="next-state-label">
<ng-container *ngIf="!stateChangePending"> <ng-container *ngIf="!stateChangePending">
@ -66,9 +69,7 @@
<!-- Detail link --> <!-- Detail link -->
<div class="poll-detail-button-wrapper"> <div class="poll-detail-button-wrapper">
<a mat-icon-button [routerLink]="pollLink" matTooltip="{{ 'More' | translate }}" *ngIf="poll.isPublished"> <a mat-icon-button [routerLink]="pollLink" matTooltip="{{ 'More' | translate }}" *ngIf="poll.isPublished">
<mat-icon class="small-icon"> <mat-icon class="small-icon">visibility</mat-icon>
visibility
</mat-icon>
</a> </a>
</div> </div>
</mat-card> </mat-card>
@ -82,34 +83,7 @@
</ng-template> </ng-template>
<ng-template #viewTemplate> <ng-template #viewTemplate>
<!-- Result Chart and legend --> <os-motion-poll-detail-content [poll]="poll" [routerLink]="pollLink"></os-motion-poll-detail-content>
<div class="poll-chart-wrapper" *osPerms="'motions.can_manage_polls'; or: poll.isPublished">
<div class="vote-legend" [routerLink]="pollLink">
<!-- any (click)-binding in an *ngFor-loop of dynamic length will break the view on iOS.
Therefore, we have to provide the trackBy-function here.
I suppose there would be debug output, but i-devices are not giving any.
The error-handling-service (PR #5131) might be helpful here. Nothing easy to find. -->
<div *ngFor="let row of reducedPollTableData; trackBy: trackByIndex" [class]="row.votingOption">
<os-icon-container [icon]="row.value[0].icon" size="large">
{{ row.value[0].amount | parsePollNumber }}
<span *ngIf="row.value[0].showPercent">
{{ row.value[0].amount | pollPercentBase: poll:'motion' }}
</span>
</os-icon-container>
</div>
</div>
<div class="doughnut-chart" [routerLink]="pollLink">
<os-charts *ngIf="showChart" type="doughnut" [data]="chartDataSubject | async"></os-charts>
</div>
</div>
<!-- In Progress hint -->
<div class="motion-couting-in-progress-hint" *osPerms="'motions.can_manage_polls'; complement: true">
<span *ngIf="poll.isFinished">
{{ 'Counting of votes is in progress ...' | translate }}
</span>
</div>
</ng-template> </ng-template>
<ng-template #emptyTemplate> <ng-template #emptyTemplate>

View File

@ -15,7 +15,6 @@ import { MotionPollDialogService } from 'app/site/motions/services/motion-poll-d
import { MotionPollPdfService } from 'app/site/motions/services/motion-poll-pdf.service'; import { MotionPollPdfService } from 'app/site/motions/services/motion-poll-pdf.service';
import { MotionPollService } from 'app/site/motions/services/motion-poll.service'; import { MotionPollService } from 'app/site/motions/services/motion-poll.service';
import { BasePollComponent } from 'app/site/polls/components/base-poll.component'; import { BasePollComponent } from 'app/site/polls/components/base-poll.component';
import { PollTableData } from 'app/site/polls/services/poll.service';
/** /**
* Component to show a motion-poll. * Component to show a motion-poll.
@ -29,8 +28,6 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll, Motio
@Input() @Input()
public set poll(value: ViewMotionPoll) { public set poll(value: ViewMotionPoll) {
this.initPoll(value); this.initPoll(value);
const chartData = this.pollService.generateChartData(value);
this.chartDataSubject.next(chartData);
} }
public get poll(): ViewMotionPoll { public get poll(): ViewMotionPoll {
@ -41,16 +38,6 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll, Motio
return `/motions/polls/${this.poll.id}`; return `/motions/polls/${this.poll.id}`;
} }
public get showChart(): boolean {
return this.pollService.showChart(this.poll);
}
public get reducedPollTableData(): PollTableData[] {
return this.pollService
.generateTableData(this.poll)
.filter(data => ['yes', 'no', 'abstain', 'votesinvalid'].includes(data.votingOption));
}
public get showPoll(): boolean { public get showPoll(): boolean {
if (this.poll) { if (this.poll) {
if ( if (

View File

@ -1,4 +1,4 @@
import { Directive, OnInit } from '@angular/core'; import { ChangeDetectorRef, Directive, OnInit } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
@ -70,11 +70,6 @@ export abstract class BasePollDetailComponentDirective<V extends ViewBasePoll, S
*/ */
public labels: Label[] = []; public labels: Label[] = [];
/**
* Subject, that holds the data for the chart.
*/
public chartDataSubject: BehaviorSubject<ChartData> = new BehaviorSubject(null);
// The observable for the votes-per-user table // The observable for the votes-per-user table
public votesDataObservable: Observable<BaseVoteData[]>; public votesDataObservable: Observable<BaseVoteData[]>;
@ -106,7 +101,8 @@ export abstract class BasePollDetailComponentDirective<V extends ViewBasePoll, S
protected pollDialog: BasePollDialogService<V, S>, protected pollDialog: BasePollDialogService<V, S>,
protected pollService: S, protected pollService: S,
protected votesRepo: BaseRepository<ViewBaseVote, BaseVote, object>, protected votesRepo: BaseRepository<ViewBaseVote, BaseVote, object>,
protected operator: OperatorService protected operator: OperatorService,
protected cd: ChangeDetectorRef
) { ) {
super(title, translate, matSnackbar); super(title, translate, matSnackbar);
this.setup(); this.setup();
@ -187,14 +183,6 @@ export abstract class BasePollDetailComponentDirective<V extends ViewBasePoll, S
*/ */
protected abstract createVotesData(): void; protected abstract createVotesData(): void;
/**
* Initializes data for the shown chart.
* Could be overwritten to implement custom chart data.
*/
protected initChartData(): void {
this.chartDataSubject.next(this.pollService.generateChartData(this.poll));
}
/** /**
* Helper-function to search for this poll and display data or create a new one. * Helper-function to search for this poll and display data or create a new one.
*/ */
@ -206,8 +194,8 @@ export abstract class BasePollDetailComponentDirective<V extends ViewBasePoll, S
if (poll) { if (poll) {
this.poll = poll; this.poll = poll;
this.createVotesData(); this.createVotesData();
this.initChartData();
this.optionsLoaded.resolve(); this.optionsLoaded.resolve();
this.cd.markForCheck();
} }
}) })
); );

View File

@ -1,5 +1,5 @@
import { ViewBasePoll } from './view-base-poll'; import { ViewBasePoll } from './view-base-poll';
export interface HasViewPolls<T extends ViewBasePoll> { export interface HasViewPolls<T extends ViewBasePoll> {
polls: T[]; polls?: T[];
} }

View File

@ -10,6 +10,7 @@ import {
MajorityMethod, MajorityMethod,
PercentBase, PercentBase,
PollColor, PollColor,
PollState,
PollType, PollType,
VOTE_UNDOCUMENTED VOTE_UNDOCUMENTED
} from 'app/shared/models/poll/base-poll'; } from 'app/shared/models/poll/base-poll';
@ -104,6 +105,7 @@ export const PollMajorityMethod: CalculableMajorityMethod[] = [
export interface PollData { export interface PollData {
pollmethod: string; pollmethod: string;
type: string; type: string;
state: PollState;
onehundred_percent_base: string; onehundred_percent_base: string;
options: PollDataOption[]; options: PollDataOption[];
votesvalid: number; votesvalid: number;

View File

@ -7,6 +7,6 @@
<h2 class="poll-title">{{ data.data.poll.title }}</h2> <h2 class="poll-title">{{ data.data.poll.title }}</h2>
</div> </div>
<div *ngIf="data.data.poll.state === PollState.Published"> <div *ngIf="data.data.poll.state === PollState.Published">
<os-motion-poll-detail-content [poll]="pollData" [chartData]="chartDataSubject" iconSize="gigantic"></os-motion-poll-detail-content> <os-motion-poll-detail-content [poll]="pollData" iconSize="gigantic"></os-motion-poll-detail-content>
</div> </div>
</ng-container> </ng-container>