Enhance table layouts
Enhance the result table layout for assignments
This commit is contained in:
parent
9d7028ea5f
commit
53b9ce73f2
@ -215,6 +215,10 @@ export class ChartsComponent extends BaseViewComponent {
|
||||
*/
|
||||
@Input()
|
||||
public pieChartOptions: ChartOptions = {
|
||||
responsive: true,
|
||||
legend: {
|
||||
position: 'left'
|
||||
},
|
||||
aspectRatio: 1
|
||||
};
|
||||
|
||||
|
@ -194,6 +194,12 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
|
||||
@Input()
|
||||
public showListOfSpeakers = true;
|
||||
|
||||
/**
|
||||
* To optionally hide the menu slot
|
||||
*/
|
||||
@Input()
|
||||
public showMenu = true;
|
||||
|
||||
/**
|
||||
* Fix value for the height of the rows in the virtual-scroll-list.
|
||||
*/
|
||||
@ -347,7 +353,7 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
|
||||
hidden.push('selection');
|
||||
}
|
||||
|
||||
if (!this.alwaysShowMenu && !this.isMobile) {
|
||||
if ((!this.alwaysShowMenu && !this.isMobile) || !this.showMenu) {
|
||||
hidden.push('menu');
|
||||
}
|
||||
|
||||
|
@ -38,6 +38,18 @@ export class AssignmentPoll extends BasePoll<
|
||||
public global_abstain: boolean;
|
||||
public description: string;
|
||||
|
||||
public get isMethodY(): boolean {
|
||||
return this.pollmethod === AssignmentPollMethod.Votes;
|
||||
}
|
||||
|
||||
public get isMethodYN(): boolean {
|
||||
return this.pollmethod === AssignmentPollMethod.YN;
|
||||
}
|
||||
|
||||
public get isMethodYNA(): boolean {
|
||||
return this.pollmethod === AssignmentPollMethod.YNA;
|
||||
}
|
||||
|
||||
public get pollmethodFields(): CalculablePollKey[] {
|
||||
if (this.pollmethod === AssignmentPollMethod.YN) {
|
||||
return ['yes', 'no'];
|
||||
|
@ -16,20 +16,29 @@
|
||||
|
||||
<!-- Detailview for poll -->
|
||||
<ng-template #viewTemplate>
|
||||
<ng-container *ngIf="isReady">
|
||||
<div *ngIf="isReady">
|
||||
<h1>{{ poll.title }}</h1>
|
||||
<span *ngIf="poll.type !== 'analog'">{{ poll.typeVerbose | translate }}</span>
|
||||
|
||||
<div *ngIf="poll.stateHasVotes">
|
||||
<div [class]="chartType === 'horizontalBar' ? 'result-wrapper-bar-chart' : 'result-wrapper-pie-chart'">
|
||||
<div class="assignment-result-wrapper">
|
||||
<!-- Result Table -->
|
||||
<table class="assignment-result-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th translate>Candidates</th>
|
||||
<th translate>Votes</th>
|
||||
<th>
|
||||
<span *ngIf="!poll.isMethodY" translate>
|
||||
Yes
|
||||
</span>
|
||||
<span *ngIf="poll.isMethodY" translate>
|
||||
Votes
|
||||
</span>
|
||||
</th>
|
||||
<th translate *ngIf="!poll.isMethodY">No</th>
|
||||
<th translate *ngIf="poll.isMethodYNA">Abstain</th>
|
||||
</tr>
|
||||
<tr *ngFor="let row of poll.tableData">
|
||||
<tr *ngFor="let row of poll.tableData" [class]="row.class">
|
||||
<td>
|
||||
<span>
|
||||
{{ row.votingOption | pollKeyVerbose | translate }}
|
||||
@ -39,23 +48,14 @@
|
||||
{{ row.votingOptionSubtitle }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div *ngFor="let vote of row.value">
|
||||
<div class="single-result" *ngIf="voteFitsMethod(vote)">
|
||||
<os-icon-container *ngIf="vote.icon" [icon]="vote.icon">
|
||||
{{ vote.vote | pollKeyVerbose | translate }}
|
||||
</os-icon-container>
|
||||
<span *ngIf="!vote.icon">
|
||||
{{ vote.vote | pollKeyVerbose | translate }}
|
||||
<td *ngFor="let vote of row.value">
|
||||
<div class="single-result" *ngIf="vote && voteFitsMethod(vote)">
|
||||
<span>
|
||||
{{ vote.amount | parsePollNumber }}
|
||||
<span *ngIf="vote.showPercent">
|
||||
{{ vote.amount | pollPercentBase: poll }}
|
||||
</span>
|
||||
|
||||
<span>
|
||||
{{ vote.amount | parsePollNumber }}
|
||||
<span *ngIf="vote.showPercent">
|
||||
{{ vote.amount | pollPercentBase: poll }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -71,12 +71,12 @@
|
||||
[labels]="candidatesLabels"
|
||||
[data]="chartDataSubject"
|
||||
[hasPadding]="false"
|
||||
[legendPosition]="isVotedPoll ? 'right' : 'top'"
|
||||
legendPosition="right"
|
||||
></os-charts>
|
||||
</div>
|
||||
|
||||
<!-- Single Votes Table -->
|
||||
<ng-container class="named-result-table" *ngIf="poll.type === 'named'">
|
||||
<div class="named-result-table" *ngIf="poll.type === 'named'">
|
||||
<h3>{{ 'Single votes' | translate }}</h3>
|
||||
<os-list-view-table
|
||||
*ngIf="votesDataObservable"
|
||||
@ -85,47 +85,41 @@
|
||||
[filterProps]="filterProps"
|
||||
[allowProjector]="false"
|
||||
[fullScreen]="false"
|
||||
[vScrollFixed]="isVotedPoll ? -1 : 60"
|
||||
[vScrollFixed]="-1"
|
||||
listStorageKey="assignment-poll-vote"
|
||||
[showListOfSpeakers]="false"
|
||||
[showMenu]="false"
|
||||
[cssClasses]="{ 'single-votes-table': true }"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div *pblNgridHeaderCellDef="'user'; col as col">
|
||||
{{ col.label | translate }}
|
||||
</div>
|
||||
<div *pblNgridHeaderCellDef="'*'; col as col">
|
||||
<div *pblNgridHeaderCellDef="'votes'; col as col">
|
||||
{{ col.label | translate }}
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div *pblNgridCellDef="'user'; row as vote">
|
||||
<b *ngIf="vote.user">{{ vote.user.getFullName() }}</b>
|
||||
<b *ngIf="!vote.user">{{ 'Anonymous' | translate }}</b>
|
||||
</div>
|
||||
<!-- Y/N/(A) -->
|
||||
<ng-container *ngIf="poll.pollmethod !== AssignmentPollMethod.Votes">
|
||||
<ng-container *ngFor="let option of poll.options">
|
||||
<div
|
||||
*pblNgridCellDef="'votes-' + option.user_id; row as vote"
|
||||
[ngClass]="voteOptionStyle[vote.votes[option.user_id].value].css"
|
||||
class="vote-field"
|
||||
>
|
||||
<mat-icon> {{ voteOptionStyle[vote.votes[option.user_id].value].icon }}</mat-icon>
|
||||
{{ vote.votes[option.user_id].valueVerbose | translate }}
|
||||
<div *ngIf="vote.user">
|
||||
{{ vote.user.getShortName() }}
|
||||
<div class="user-subtitle" *ngIf="vote.user.getLevelAndNumber()">
|
||||
{{ vote.user.getLevelAndNumber() }}
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<!-- Votes method -->
|
||||
<ng-container *ngIf="poll.pollmethod === AssignmentPollMethod.Votes">
|
||||
<div *pblNgridCellDef="'votes'; row as vote">
|
||||
<div *ngFor="let candidate of vote.votes">{{ candidate }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div *ngIf="!vote.user">
|
||||
{{ 'Anonymous' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *pblNgridCellDef="'votes'; row as vote" >
|
||||
<div class="single-vote-result" *ngFor="let candidate of vote.votes">{{ candidate }}</div>
|
||||
</div>
|
||||
</os-list-view-table>
|
||||
<div *ngIf="!votesDataObservable">
|
||||
{{ 'The individual votes were anonymized.' | translate }}
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Meta Infos -->
|
||||
@ -142,7 +136,7 @@
|
||||
{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}
|
||||
</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<!-- More Menu -->
|
||||
|
@ -1,73 +1,69 @@
|
||||
@import '~assets/styles/variables.scss';
|
||||
@import '~assets/styles/poll-colors.scss';
|
||||
|
||||
%assignment-result-wrapper {
|
||||
.assignment-result-wrapper {
|
||||
margin-top: 2em;
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
}
|
||||
|
||||
.result-wrapper-bar-chart {
|
||||
@extend %assignment-result-wrapper;
|
||||
grid-template-areas:
|
||||
'results'
|
||||
'chart'
|
||||
'names';
|
||||
}
|
||||
.assignment-result-table {
|
||||
border-collapse: collapse;
|
||||
|
||||
.result-wrapper-pie-chart {
|
||||
@extend %assignment-result-wrapper;
|
||||
grid-template-areas:
|
||||
'chart'
|
||||
'results'
|
||||
'names';
|
||||
}
|
||||
th {
|
||||
text-align: left;
|
||||
font-weight: initial;
|
||||
}
|
||||
|
||||
@include desktop {
|
||||
.result-wrapper-pie-chart {
|
||||
grid-template-areas:
|
||||
'results chart'
|
||||
'names names';
|
||||
grid-template-columns: 2fr 1fr;
|
||||
tr {
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
tr.sums {
|
||||
border-bottom: none;
|
||||
td {
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.user + .sums {
|
||||
td {
|
||||
padding-top: 4em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pie-chart {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.assignment-result-table {
|
||||
grid-area: results;
|
||||
th {
|
||||
text-align: left;
|
||||
font-weight: initial;
|
||||
}
|
||||
|
||||
tr {
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.single-result {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.assignment-result-chart {
|
||||
grid-area: chart;
|
||||
}
|
||||
|
||||
.pie-chart {
|
||||
max-width: 300px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
.single-vote-result + .single-vote-result {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.named-result-table {
|
||||
grid-area: names;
|
||||
.mat-form-field {
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.single-votes-table {
|
||||
display: block;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.vote-field {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding-right: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.assignment-poll-meta {
|
||||
@ -76,22 +72,6 @@
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.single-votes-table {
|
||||
display: block;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.openslides-theme .pbl-ngrid-row:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.openslides-theme os-list-view-table os-sort-filter-bar .custom-table-header {
|
||||
&,
|
||||
.action-buttons .input-container input {
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
|
||||
.voted-yes {
|
||||
color: $votes-yes-color;
|
||||
}
|
||||
@ -104,6 +84,18 @@
|
||||
color: $votes-abstain-color;
|
||||
}
|
||||
|
||||
// theme
|
||||
.openslides-theme .pbl-ngrid-row:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.openslides-theme os-list-view-table os-sort-filter-bar .custom-table-header {
|
||||
&,
|
||||
.action-buttons .input-container input {
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
|
||||
.openslides-theme .pbl-ngrid-no-data {
|
||||
top: 10%;
|
||||
}
|
||||
@ -123,11 +115,3 @@
|
||||
border-right: 1px solid #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
.vote-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
@ -11,9 +11,7 @@ import { AssignmentPollRepositoryService } from 'app/core/repositories/assignmen
|
||||
import { AssignmentVoteRepositoryService } from 'app/core/repositories/assignments/assignment-vote-repository.service';
|
||||
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
|
||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||
import { ChartType } from 'app/shared/components/charts/charts.component';
|
||||
import { AssignmentPollMethod } from 'app/shared/models/assignments/assignment-poll';
|
||||
import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-detail.component';
|
||||
import { VotingResult } from 'app/site/polls/models/view-base-poll';
|
||||
import { PollService } from 'app/site/polls/services/poll.service';
|
||||
@ -27,8 +25,6 @@ import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewAssignmentPoll> {
|
||||
public AssignmentPollMethod = AssignmentPollMethod;
|
||||
|
||||
public columnDefinitionSingleVotes: PblColumnDefinition[];
|
||||
|
||||
public filterProps = ['user.getFullName'];
|
||||
@ -41,10 +37,6 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
||||
return this._chartType;
|
||||
}
|
||||
|
||||
public get isVotedPoll(): boolean {
|
||||
return this.poll.pollmethod === AssignmentPollMethod.Votes;
|
||||
}
|
||||
|
||||
private _chartType: ChartType = 'horizontalBar';
|
||||
|
||||
public constructor(
|
||||
@ -58,67 +50,48 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
||||
pollDialog: AssignmentPollDialogService,
|
||||
pollService: PollService,
|
||||
votesRepo: AssignmentVoteRepositoryService,
|
||||
private operator: OperatorService,
|
||||
private viewport: ViewportService
|
||||
private operator: OperatorService
|
||||
) {
|
||||
super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog, pollService, votesRepo);
|
||||
}
|
||||
|
||||
protected createVotesData(): void {
|
||||
const votes = {};
|
||||
let i = -1;
|
||||
|
||||
const definitions: PblColumnDefinition[] = [
|
||||
{
|
||||
prop: 'user',
|
||||
label: 'Participant',
|
||||
width: '180px',
|
||||
pin: this.viewport.isMobile ? undefined : 'start'
|
||||
width: '40%',
|
||||
minWidth: 300
|
||||
},
|
||||
{
|
||||
prop: 'votes',
|
||||
label: 'Votes',
|
||||
width: '60%',
|
||||
minWidth: 300
|
||||
}
|
||||
];
|
||||
if (this.isVotedPoll) {
|
||||
definitions.push(this.getVoteColumnDefinition('votes', 'Votes'));
|
||||
}
|
||||
|
||||
/**
|
||||
* builds an object of the following form:
|
||||
* {
|
||||
* userId: {
|
||||
* user: ViewUser,
|
||||
* votes: { candidateId: voteValue } // for YN(A)
|
||||
* | candidate_name[] // for Votes
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
for (const option of this.poll.options) {
|
||||
if (!this.isVotedPoll) {
|
||||
definitions.push(this.getVoteColumnDefinition('votes-' + option.user_id, option.user.getFullName()));
|
||||
}
|
||||
|
||||
for (const vote of option.votes) {
|
||||
// if poll was pseudoanonymized, use a negative index to not interfere with
|
||||
// possible named votes (although this should never happen)
|
||||
const userId = vote.user_id || --i;
|
||||
const userId = vote.user_id;
|
||||
if (!votes[userId]) {
|
||||
votes[userId] = {
|
||||
user: vote.user,
|
||||
votes: this.isVotedPoll ? [] : {}
|
||||
votes: []
|
||||
};
|
||||
}
|
||||
// on votes method, we fill an array with all chosen candidates
|
||||
// on YN(A) we map candidate ids to the vote
|
||||
if (this.isVotedPoll) {
|
||||
if (vote.weight > 0) {
|
||||
|
||||
if (vote.weight > 0) {
|
||||
if (this.poll.isMethodY) {
|
||||
if (vote.value === 'Y') {
|
||||
votes[userId].votes.push(option.user.getFullName());
|
||||
} else if (vote.value === 'N') {
|
||||
votes[userId].votes.push(this.translate.instant('No'));
|
||||
} else if (vote.value === 'A') {
|
||||
votes[userId].votes.push(this.translate.instant('Abstain'));
|
||||
} else {
|
||||
votes[userId].votes.push(this.voteValueToLabel(vote.value));
|
||||
}
|
||||
} else {
|
||||
votes[userId].votes.push(`${option.user.getShortName()}: ${this.voteValueToLabel(vote.value)}`);
|
||||
}
|
||||
} else {
|
||||
votes[userId].votes[option.user_id] = vote;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -129,17 +102,20 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
||||
this.isReady = true;
|
||||
}
|
||||
|
||||
private getVoteColumnDefinition(prop: string, label: string): PblColumnDefinition {
|
||||
return {
|
||||
prop: prop,
|
||||
label: label,
|
||||
minWidth: 80,
|
||||
width: 'auto'
|
||||
};
|
||||
private voteValueToLabel(vote: 'Y' | 'N' | 'A'): string {
|
||||
if (vote === 'Y') {
|
||||
return this.translate.instant('Yes');
|
||||
} else if (vote === 'N') {
|
||||
return this.translate.instant('No');
|
||||
} else if (vote === 'A') {
|
||||
return this.translate.instant('Abstain');
|
||||
} else {
|
||||
throw new Error(`voteValueToLabel received illegal arguments: ${vote}`);
|
||||
}
|
||||
}
|
||||
|
||||
protected initChartData(): void {
|
||||
if (this.isVotedPoll) {
|
||||
if (this.poll.isMethodY) {
|
||||
this._chartType = 'doughnut';
|
||||
this.chartDataSubject.next(this.pollService.generateCircleChartData(this.poll));
|
||||
} else {
|
||||
@ -152,11 +128,11 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
|
||||
}
|
||||
|
||||
public voteFitsMethod(result: VotingResult): boolean {
|
||||
if (this.poll.pollmethod === AssignmentPollMethod.Votes) {
|
||||
if (this.poll.isMethodY) {
|
||||
if (result.vote === 'abstain' || result.vote === 'no') {
|
||||
return false;
|
||||
}
|
||||
} else if (this.poll.pollmethod === AssignmentPollMethod.YN) {
|
||||
} else if (this.poll.isMethodYN) {
|
||||
if (result.vote === 'abstain') {
|
||||
return false;
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll, AssignmentP
|
||||
const tableData: PollTableData[] = this.options.map(candidate => ({
|
||||
votingOption: candidate.user.short_name,
|
||||
votingOptionSubtitle: candidate.user.getLevelAndNumber(),
|
||||
|
||||
class: 'user',
|
||||
value: this.voteTableKeys.map(
|
||||
key =>
|
||||
({
|
||||
@ -84,6 +84,7 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll, AssignmentP
|
||||
tableData.push(
|
||||
...this.sumTableKeys.map(key => ({
|
||||
votingOption: key.vote,
|
||||
class: 'sums',
|
||||
value: [
|
||||
{
|
||||
amount: this[key.vote],
|
||||
@ -93,7 +94,6 @@ export class ViewAssignmentPoll extends ViewBasePoll<AssignmentPoll, AssignmentP
|
||||
]
|
||||
}))
|
||||
);
|
||||
|
||||
return tableData;
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
import { PollFormComponent } from './poll-form.component';
|
||||
|
||||
fdescribe('PollFormComponent', () => {
|
||||
describe('PollFormComponent', () => {
|
||||
let component: PollFormComponent<any>;
|
||||
let fixture: ComponentFixture<PollFormComponent<any>>;
|
||||
|
||||
|
@ -17,6 +17,7 @@ export enum PollClassType {
|
||||
export interface PollTableData {
|
||||
votingOption: string;
|
||||
votingOptionSubtitle?: string;
|
||||
class?: string;
|
||||
value: VotingResult[];
|
||||
}
|
||||
|
||||
@ -162,10 +163,10 @@ export abstract class ViewBasePoll<
|
||||
public abstract get percentBaseVerbose(): string;
|
||||
|
||||
public get showAbstainPercent(): boolean {
|
||||
return this.onehundred_percent_base === PercentBase.YNA;
|
||||
return this.poll.onehundred_percent_base === PercentBase.YNA;
|
||||
}
|
||||
|
||||
public abstract readonly pollClassType: PollClassType;
|
||||
public abstract readonly pollClassType: 'motion' | 'assignment';
|
||||
|
||||
public canBeVotedFor: () => boolean;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user