Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add filters to poll single votes #3978

Merged
merged 31 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
621f14c
Add filters to poll single votes
bastianjoel Aug 5, 2024
5b51823
Add chart component
bastianjoel Aug 6, 2024
b9a29be
Add filters enabled check
bastianjoel Aug 6, 2024
5fcd6f5
Cleanup motion poll detail content
bastianjoel Aug 6, 2024
8c69ae4
Implement chart
bastianjoel Aug 7, 2024
230f8c3
Fix linter
bastianjoel Aug 7, 2024
4ef9daf
Change visual and layout
Elblinator Aug 7, 2024
f9a17f8
Merge branch 'main' into 3899-poll-filter-bar-chart
Elblinator Aug 14, 2024
c6c9bd3
Change Layout
Elblinator Aug 14, 2024
97a28f9
Merge branch 'main' into 3899-poll-filter-bar-chart
bastianjoel Aug 14, 2024
58d1bc5
Readd missing check
bastianjoel Aug 14, 2024
0f78801
Hide filters for non nomimal polls
bastianjoel Aug 14, 2024
caf7986
Fix filter for assignment polls
bastianjoel Aug 14, 2024
b7380c1
Display vote category names
bastianjoel Aug 14, 2024
b464b3f
Add title and mat-divider
Elblinator Aug 15, 2024
a221d39
Fix too small container
Elblinator Aug 15, 2024
00d73bc
updated chart
Elblinator Aug 15, 2024
d811005
Remove variable
Elblinator Aug 15, 2024
e975603
Clean up
Elblinator Aug 15, 2024
9c20850
Adjust array
Elblinator Aug 15, 2024
29a153f
Revert "Adjust array"
bastianjoel Aug 15, 2024
456ef94
Finish design
Elblinator Aug 15, 2024
8a8009e
Change call for `Ballot cast` string
Elblinator Aug 15, 2024
09e1c92
Fix margin from list
Elblinator Aug 16, 2024
7185a0c
Fix string for Ballots cast
Elblinator Aug 16, 2024
d8bc9f6
Move scss classes around
Elblinator Aug 16, 2024
fa42700
Remaining fixes
bastianjoel Aug 28, 2024
e059a96
Use percent base for vote values
bastianjoel Aug 28, 2024
dd64b46
Filter abstain
bastianjoel Aug 28, 2024
d65da76
Fix change requests
bastianjoel Aug 28, 2024
85e9d55
Merge branch 'main' into 3899-poll-filter-bar-chart
bastianjoel Aug 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<mat-divider class="margin-top-bottom"></mat-divider>
@if (filtersEnabled && totalAmount > 0) {
<div class="vote-char-container">
<h2 class="margin-bottom-10">{{ 'Filtered single votes:' | translate }}</h2>
<div class="vote-chart-bar">
@for (cat of voteAmounts; track cat.value) {
@if (cat.amount > 0 && !cat.hiddenInBase) {
<div
class="segment"
[style.background]="cat.backgroundColor"
[style.width.%]="(cat.weightedAmount / totalAmountWeighted) * 100"
></div>
}
}
</div>
<div class="results">
<div class="grid-all-votes-area">{{ 'Valid votes' | translate }} {{ validAmount }}</div>
<div class="grid-options-area">
@for (cat of voteAmounts; track cat.value) {
<span class="one-option">
<div class="box" [style.background]="cat.backgroundColor"></div>
{{ cat.name | translate }}: {{ cat.weightedAmount }}
@if (cat.amount > 0 && !cat.hiddenInBase) {
({{ ((cat.weightedAmount / totalAmountWeighted) * 100).toFixed(2) }}%)
}
</span>
}
</div>
</div>
@if (usesVoteWeight) {
<div class="results margin-bottom-10">
<div class="user-subtitle">
<span class="no-space">{{ 'Ballots cast' | translate }}: {{ totalAmount }} (</span>
<div class="categories">
@for (cat of voteAmounts; track cat.value) {
<span>{{ cat.name | translate }}: {{ cat.amount }}</span>
<span class="seperator">&middot;</span>
}
</div>
<span class="no-space">)</span>
</div>
</div>
}
</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
.vote-chart-bar {
display: flex;
align-items: center;
height: 70px;
.segment {
position: relative;
text-align: center;
height: 50px;
border: 1px solid white;
transition: 0.3s;
.below {
position: absolute;
width: 100%;
top: 58px;
}
}
.segment:hover {
border-color: transparent;
border-right-width: 0;
border-left-width: 0;
}
}

.results {
display: grid;
grid-gap: 10px !important;
margin: 0;
grid-template-areas: 'votes options';
grid-template-columns: auto auto;
@media (max-width: 700px) {
grid-template-areas: 'votes votes' 'options options';
grid-template-columns: max-content;
}
.categories {
display: inline-block;
span:last-of-type {
display: none;
}
.seperator {
padding: 0 4px;
}
}
}

.grid-all-votes-area {
grid-area: votes;
}

.grid-options-area {
grid-area: options;
display: flex;
justify-content: end;
}

.box {
height: 15px;
width: 15px;
display: inline-block;
}

.margin-top-bottom {
margin-top: 50px;
margin-bottom: 10px;
}

.margin-bottom-10 {
margin-bottom: 10px;
}

.one-option {
margin-right: 20px;
}

.one-option:last-of-type {
margin-right: 0;
}

.no-space {
white-space: pre;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit } from '@angular/core';
import { PollPercentBase, VoteValue, VoteValueVerbose } from 'src/app/domain/models/poll';
import { ThemeService } from 'src/app/site/services/theme.service';
import { BaseUiComponent } from 'src/app/ui/base/base-ui-component';

import { ViewPoll, ViewVote } from '../../../../pages/polls';
import { MeetingSettingsService } from '../../../../services/meeting-settings.service';
import { VotesFilterService } from '../../services/votes-filter.service';

interface VoteAmount {
value: VoteValue;
name: string;
amount: number;
hiddenInBase: boolean;
weightedAmount: number;
backgroundColor: string;
}

@Component({
selector: `os-poll-filted-votes-chart`,
templateUrl: `./poll-filtered-votes-chart.component.html`,
styleUrls: [`./poll-filtered-votes-chart.component.scss`],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PollFilteredVotesChartComponent extends BaseUiComponent implements OnInit {
@Input()
public poll: ViewPoll;

public usesVoteWeight = false;
public validAmount = 0;
public totalAmount = 0;
public totalAmountWeighted = 0;
public voteAmounts: VoteAmount[] = [];

public get filtersEnabled(): boolean {
return this.filterService.filterStack.length > 0;
}

private meetingSettings: MeetingSettingsService = inject(MeetingSettingsService);
private themeService: ThemeService = inject(ThemeService);
private filterService: VotesFilterService = inject(VotesFilterService);
private cd: ChangeDetectorRef = inject(ChangeDetectorRef);

public ngOnInit(): void {
this.subscriptions.push(this.filterService.outputObservable.subscribe(votes => this.onVotesUpdated(votes)));
}

private onVotesUpdated(votes: ViewVote[]): void {
this.voteAmounts = [];
const voteValues: VoteValue[] = this.poll.isMethodYN ? [`Y`, `N`] : [`Y`, `N`, `A`];
const baseVoteValues: VoteValue[] =
this.poll.onehundred_percent_base === PollPercentBase.YN ? [`Y`, `N`] : [`Y`, `N`, `A`];
const countedVotes = votes.filter(v => baseVoteValues.indexOf(v.value) !== -1);
for (const i in voteValues) {
const voteValue = voteValues[i];
this.voteAmounts.push({
value: voteValue,
name: VoteValueVerbose[voteValue],
hiddenInBase: baseVoteValues.indexOf(voteValue) === -1,
amount: votes.reduce((acc, curr) => acc + +(curr.value === voteValue), 0),
weightedAmount: votes.reduce((acc, curr) => acc + (curr.value === voteValue ? +curr.weight : 0), 0),
backgroundColor: this.themeService.getPollColor(VoteValueVerbose[voteValue].toLowerCase())
});
}

this.totalAmount = votes.length;
this.totalAmountWeighted = countedVotes.reduce((acc, curr) => acc + +curr.weight, 0);
this.validAmount = votes.reduce((acc, curr) => acc + +curr.weight, 0);

this.usesVoteWeight =
this.meetingSettings.instant(`users_enable_vote_weight`) || this.totalAmount !== this.validAmount;

this.cd.markForCheck();
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
<div *osPerms="permission.userCanSee">
@if (isViewingThis) {
<div>
<os-list [filterProps]="filterProps" [fullScreen]="false" [listObservable]="votesDataObservable">
<os-list
[filterProps]="filterProps"
[filterService]="parent.poll.type === 'named' ? filter : null"
[fullScreen]="false"
[listObservable]="votesDataObservable"
>
<!-- Content -->
<div *osScrollingTableCell="'user'; row as vote">
<div *osScrollingTableCellLabel>{{ 'Participant' | translate }}</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { PollContentObject } from 'src/app/domain/models/poll';

import { BasePollDetailComponent, BaseVoteData } from '../../base/base-poll-detail.component';
import { PollService } from '../../services/poll.service';
import { VotesFilterService } from '../../services/votes-filter.service';

@Component({
selector: `os-votes-table`,
Expand Down Expand Up @@ -51,6 +52,8 @@ export class VotesTableComponent {

private _votesDataObservable!: Observable<BaseVoteData[]>;

public constructor(public filter: VotesFilterService) {}

public getVoteIcon(voteValue: string): string {
return this.parent.voteOptionStyle[voteValue]?.icon;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDividerModule } from '@angular/material/divider';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
Expand All @@ -21,6 +22,7 @@ import { ChartComponent } from './components/chart/chart.component';
import { CheckInputComponent } from './components/check-input/check-input.component';
import { EntitledUsersTableComponent } from './components/entitled-users-table/entitled-users-table.component';
import { PollCannotVoteMessageComponent } from './components/poll-cannot-vote-message/poll-cannot-vote-message.component';
import { PollFilteredVotesChartComponent } from './components/poll-filtered-votes-chart/poll-filtered-votes-chart.component';
import { PollProgressComponent } from './components/poll-progress/poll-progress.component';
import { SingleOptionChartTableComponent } from './components/single-option-chart-table/single-option-chart-table.component';
import { VotesTableComponent } from './components/votes-table/votes-table.component';
Expand All @@ -32,6 +34,7 @@ const MODULES = [PollServiceModule, VotingPrivacyDialogModule];
const PIPES = [PollKeyVerbosePipe, PollPercentBasePipe, PollParseNumberPipe];
const COMPONENTS = [
PollProgressComponent,
PollFilteredVotesChartComponent,
ChartComponent,
CheckInputComponent,
EntitledUsersTableComponent,
Expand All @@ -51,6 +54,7 @@ const COMPONENTS = [
MatInputModule,
MatIconModule,
MatCheckboxModule,
MatDividerModule,
MatRadioModule,
ReactiveFormsModule,
MatTooltipModule,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Injectable } from '@angular/core';
import { BaseFilterListService, OsFilter } from 'src/app/site/base/base-filter.service';
import { ActiveFiltersService } from 'src/app/site/services/active-filters.service';

import { GroupControllerService } from '../../../pages/participants';
import { StructureLevelControllerService } from '../../../pages/participants/pages/structure-levels/services/structure-level-controller.service';
import { ViewVote } from '../../../pages/polls';
import { PollServiceModule } from '../services/poll-service.module';

@Injectable({
providedIn: PollServiceModule
})
export class VotesFilterService extends BaseFilterListService<any> {
protected storageKey = `VotesEntry`;

private groupFilterOptions: OsFilter<ViewVote> = {
property: `groupIds`,
label: `Groups`,
options: []
};

private structureLevelFilterOptions: OsFilter<ViewVote> = {
property: `structureLevelIds`,
label: `Structure level`,
options: []
};

public constructor(
store: ActiveFiltersService,
groupRepo: GroupControllerService,
structureRepo: StructureLevelControllerService
) {
super(store);

this.updateFilterForRepo({
repo: groupRepo,
filter: this.groupFilterOptions
});
this.updateFilterForRepo({
repo: structureRepo,
filter: this.structureLevelFilterOptions
});
}

protected getFilterDefinitions(): OsFilter<any>[] {
return [].concat(this.groupFilterOptions, this.structureLevelFilterOptions);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ export class AssignmentPollDetailComponent
if (!votes[token]) {
votes[token] = {
user: vote.user,
groupIds: vote.user?.group_ids(),
structureLevelIds: vote.user?.structure_level_ids(),
votes: []
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,29 @@
@if (poll) {
<div class="result-wrapper">
@if (hasResults && canSeeResults) {
<os-single-option-chart-table
[iconSize]="iconSize"
[poll]="poll"
[pollService]="motionPollService"
[shouldShowEntitled]="isPercentBaseEntitled"
[shouldShowEntitledPresent]="isPercentBaseEntitledPresent"
[shouldShowHead]="true"
[tableData]="tableData"
></os-single-option-chart-table>
}
<!-- No results yet -->
@if (!hasResults) {
@if (hasResults) {
@if (canSeeResults) {
<os-single-option-chart-table
[iconSize]="iconSize"
[poll]="poll"
[pollService]="motionPollService"
[shouldShowEntitled]="isPercentBaseEntitled"
[shouldShowEntitledPresent]="isPercentBaseEntitledPresent"
[shouldShowHead]="true"
[tableData]="tableData"
></os-single-option-chart-table>
} @else {
<div>
<i>
{{ 'Counting of votes is in progress ...' | translate }}
</i>
</div>
}
} @else {
<div>
<i>
{{ 'No results yet.' | translate }}
</i>
</div>
}
<!-- Has results, but user cannot see -->
@if (hasResults && !canSeeResults) {
<div>
<i>
{{ 'Counting of votes is in progress ...' | translate }}
</i>
</div>
}
</div>
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,13 @@ <h1>{{ poll.title | translate }}</h1>
</span>
</div>
<os-motion-poll-detail-content [poll]="poll"></os-motion-poll-detail-content>

<!-- Named table: only show if votes are present -->
@if (showResults && poll.stateHasVotes && poll.isEVoting) {
@if (poll.type === 'named') {
<os-poll-filted-votes-chart [poll]="poll"></os-poll-filted-votes-chart>
}

<mat-tab-group (selectedTabChange)="onTabChange()">
<mat-tab label="{{ 'Single votes' | translate }}">
<div class="named-result-table">
Expand Down
Loading