diff --git a/client/src/app/core/services/http.service.ts b/client/src/app/core/services/http.service.ts index ce3711585..5a549858c 100644 --- a/client/src/app/core/services/http.service.ts +++ b/client/src/app/core/services/http.service.ts @@ -131,19 +131,19 @@ export class HttpService { /** * Exectures a post on a url with a certain object - * @param url string of the url to send semothing to - * @param data The data to send + * @param url The url to send the request to. + * @param data An optional payload for the request. * @param header optional HTTP header if required * @returns A promise holding a generic */ - public async post(url: string, data: any, header?: HttpHeaders): Promise { + public async post(url: string, data?: any, header?: HttpHeaders): Promise { return await this.send(url, HTTPMethod.POST, data, header); } /** * Exectures a put on a url with a certain object - * @param url string of the url to send semothing to - * @param data the object that should be send + * @param url The url to send the request to. + * @param data The payload for the request. * @param header optional HTTP header if required * @returns A promise holding a generic */ @@ -153,8 +153,8 @@ export class HttpService { /** * Exectures a put on a url with a certain object - * @param url the url that should be called - * @param data: The data to send + * @param url The url to send the request to. + * @param data: The payload for the request. * @param header optional HTTP header if required * @returns A promise holding a generic */ @@ -164,7 +164,7 @@ export class HttpService { /** * Makes a delete request. - * @param url the url that should be called + * @param url The url to send the request to. * @param data An optional data to send in the requestbody. * @param header optional HTTP header if required * @returns A promise holding a generic diff --git a/client/src/app/site/motions/components/motion-detail/motion-detail.component.html b/client/src/app/site/motions/components/motion-detail/motion-detail.component.html index 5fb2edcaa..b47dd608b 100644 --- a/client/src/app/site/motions/components/motion-detail/motion-detail.component.html +++ b/client/src/app/site/motions/components/motion-detail/motion-detail.component.html @@ -79,7 +79,7 @@ - + info @@ -93,9 +93,6 @@ - - - @@ -109,6 +106,9 @@ + + + @@ -150,7 +150,7 @@ - +
@@ -166,75 +166,97 @@
- -
- + +
-
-

Supporters

-
    -
  • {{ supporter.full_name }}
  • -
+
+

Supporters

+ + + + + + +

+ + {{ supporter.full_name }} + +

+
-
-
- -

State

- {{ motion.state }} -
-
-
- - - {{ motion.state }} - - {{ state }} - - - replay - Reset State - - - -
+
+

State

+ + + + + + + {{ motion.state.name | translate }} + + +
-
- - - - {{ recommendation.recommendation_label | translate }} - - - - replayReset recommendation - - - +
+

{{ recommender }}

+ + + + + + + {{ motion.recommendation ? (motion.recommendation.recommendation_label | translate) : ('not set' | translate) }} +
-
-
-

Category

- {{ motion.category }} -
-
- -
+ +
+

Category

+ + + + + + {{ motion.category ? motion.category : ('not set' | translate) }} +
- +
format_list_numbered -
@@ -293,21 +315,15 @@
-
-
-

{{motion.title}}

-
- - - +
+ + -
- - The assembly may decide: + {{ preamble | translate }}
@@ -343,17 +359,18 @@ [init]="tinyMceSettings" *ngIf="editMotion" > -
- - - - + +
+ + + +
diff --git a/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts b/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts index ff5341da2..3b27e23f3 100644 --- a/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts +++ b/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { MatDialog, MatExpansionPanel, MatSnackBar, MatSelectChange, MatCheckboxChange } from '@angular/material'; +import { MatDialog, MatExpansionPanel, MatSnackBar, MatCheckboxChange } from '@angular/material'; import { Category } from '../../../../shared/models/motions/category'; import { ViewportService } from '../../../../core/services/viewport.service'; @@ -28,6 +28,7 @@ import { StatuteParagraphRepositoryService } from '../../services/statute-paragr import { ConfigService } from '../../../../core/services/config.service'; import { Workflow } from 'app/shared/models/motions/workflow'; import { take, takeWhile, multicast, skipWhile } from 'rxjs/operators'; +import { LocalPermissionsService } from '../../services/local-permissions.service'; /** * Component for the motion detail view @@ -94,11 +95,22 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { private _motion: ViewMotion; /** - * Value of the configuration variable `motions_statutes_enabled` - are statutes enabled? + * Value of the config variable `motions_statutes_enabled` - are statutes enabled? * @TODO replace by direct access to config variable, once it's available from the templates */ public statutesEnabled: boolean; + /** + * Value of the config variable `motions_min_supporters` + */ + public minSupporters: number; + + /** + * Value of the config variable `motions_preamble` + */ + public preamble: string; + + /** * Copy of the motion that the user might edit */ @@ -154,6 +166,11 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { */ public supporterObserver: BehaviorSubject; + /** + * Determine if the name of supporters are visible + */ + public showSupporters = false; + /** * Value for os-motion-detail-diff: when this is set, that component scrolls to the given change */ @@ -193,6 +210,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { translate: TranslateService, matSnackBar: MatSnackBar, public vp: ViewportService, + public perms: LocalPermissionsService, private op: OperatorService, private router: Router, private route: ActivatedRoute, @@ -226,11 +244,22 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { this.workflowObserver.next(DS.getAll(Workflow)); } }); + // load config variables this.configService.get('motions_statutes_enabled').subscribe( (enabled: boolean): void => { this.statutesEnabled = enabled; } ); + this.configService.get('motions_min_supporters').subscribe( + (supporters: number): void => { + this.minSupporters = supporters; + } + ); + this.configService.get('motions_preamble').subscribe( + (preamble: string): void => { + this.preamble = preamble; + } + ); } /** @@ -343,7 +372,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { * The AutoUpdate-Service should see a change once it arrives and show it * in the list view automatically * - * TODO: state is not yet saved. Need a special "put" command. Repo should handle this. */ public async saveMotion(): Promise { const newMotionValues = { ...this.metaInfoForm.value, ...this.contentForm.value }; @@ -410,7 +438,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { /** * Sets the motions line numbering mode - * @param mode Needs to fot to the enum defined in ViewMotion + * @param mode Needs to got the enum defined in ViewMotion */ public setLineNumberingMode(mode: LineNumberingMode): void { this.motion.lnMode = mode; @@ -578,19 +606,49 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit { } /** - * Executed after selecting a state - * @param selection MatSelectChange that contains the workflow id + * Supports the motion (as requested user) */ - public onChangeState(selection: MatSelectChange): void { - this.repo.setState(this.motion, selection.value); + public support(): void { + this.repo.support(this.motion).then(null, this.raiseError); } /** - * Executed after selecting the recommenders state - * @param selection MatSelectChange that contains the workflow id + * Unsupports the motion */ - public onChangerRecommenderState(selection: MatSelectChange): void { - this.repo.setRecommenderState(this.motion, selection.value); + public unsupport(): void { + this.repo.unsupport(this.motion).then(null, this.raiseError); + } + + /** + * Opens the dialog with all supporters. + * TODO: open dialog here! + */ + public openSupportersDialog(): void { + this.showSupporters = !this.showSupporters; + } + + /** + * Sets the state + * @param id Motion state id + */ + public setState(id: number): void { + this.repo.setState(this.motion, id); + } + + /** + * Sets the recommendation + * @param id Motion recommendation id + */ + public setRecommendation(id: number): void { + this.repo.setRecommendation(this.motion, id); + } + + /** + * Sets the category for current motion + * @param id Motion category id + */ + public setCategory(id: number): void { + this.repo.setCatetory(this.motion, id); } /** diff --git a/client/src/app/site/motions/components/motion-list/motion-list.component.html b/client/src/app/site/motions/components/motion-list/motion-list.component.html index d8ea32e84..c3f0a0755 100644 --- a/client/src/app/site/motions/components/motion-list/motion-list.component.html +++ b/client/src/app/site/motions/components/motion-list/motion-list.component.html @@ -65,6 +65,20 @@ by {{ motion.submitters }} +
+ + + {{ motion.state.name | translate }} + + + + + {{ motion.recommendation.recommendation_label | translate }} +
@@ -72,13 +86,10 @@ State - - - - {{ motion.state }} - + +
+ device_hub {{ motion.category }} +
@@ -98,7 +109,7 @@ + *matRowDef="let row; columns: getColumnDefinition()" class="lg"> diff --git a/client/src/app/site/motions/components/motion-list/motion-list.component.scss b/client/src/app/site/motions/components/motion-list/motion-list.component.scss index a62c88def..85c3e8fcf 100644 --- a/client/src/app/site/motions/components/motion-list/motion-list.component.scss +++ b/client/src/app/site/motions/components/motion-list/motion-list.component.scss @@ -1,8 +1,8 @@ /** css hacks https://codepen.io/edge0703/pen/iHJuA */ .innerTable { display: inline-block; - vertical-align: middle; - line-height: normal; + vertical-align: top; + line-height: 150%; } .os-listview-table { @@ -23,10 +23,10 @@ .motion-list-title { font-weight: bold; + font-size: 16px; } .motion-list-from { - margin-top: 5px; color: rgba(0, 0, 0, 0.5); font-size: 90%; } diff --git a/client/src/app/site/motions/models/view-motion.ts b/client/src/app/site/motions/models/view-motion.ts index 08a2fa304..8cbcfd5bc 100644 --- a/client/src/app/site/motions/models/view-motion.ts +++ b/client/src/app/site/motions/models/view-motion.ts @@ -227,6 +227,11 @@ export class ViewMotion extends BaseViewModel { this._block = block; // TODO: Should be set using a a config variable + /*this._configService.get('motions_default_line_numbering').subscribe( + (mode: string): void => { + this.lnMode = LineNumberingMode.Outside; + } + );*/ this.lnMode = LineNumberingMode.Outside; this.crMode = ChangeRecoMode.Original; this.lineLength = 80; @@ -265,9 +270,9 @@ export class ViewMotion extends BaseViewModel { this.updateItem(update as Item); } else if (update instanceof MotionBlock) { this.updateMotionBlock(update); + } else if (update instanceof User) { + this.updateUser(update as User); } - // TODO: There is no way (yet) to add Submitters to a motion - // Thus, this feature could not be tested } /** @@ -310,6 +315,23 @@ export class ViewMotion extends BaseViewModel { } } + /** + * Update routine for the agenda Item + * @param update potentially the changed agenda Item. Needs manual verification + */ + public updateUser(update: User): void { + if (this.motion) { + if (this.motion.submitters && this.motion.submitters.findIndex(user => user.user_id === update.id)) { + const userIndex = this.submitters.findIndex(user => user.id === update.id); + this.submitters[userIndex] = update as User; + } + if (this.motion.supporters_id && this.motion.supporters_id.includes(update.id)) { + const userIndex = this.supporters.findIndex(user => user.id === update.id); + this.supporters[userIndex] = update as User; + } + } + } + public hasSupporters(): boolean { return !!(this.supporters && this.supporters.length > 0); } diff --git a/client/src/app/site/motions/services/local-permissions.service.spec.ts b/client/src/app/site/motions/services/local-permissions.service.spec.ts new file mode 100644 index 000000000..2a160f368 --- /dev/null +++ b/client/src/app/site/motions/services/local-permissions.service.spec.ts @@ -0,0 +1,13 @@ +import { TestBed } from '@angular/core/testing'; + +import { LocalPermissionsService } from './local-permissions.service'; +import { E2EImportsModule } from '../../../../e2e-imports.module'; + +describe('LocalPermissionsService', () => { + beforeEach(() => TestBed.configureTestingModule({ imports: [E2EImportsModule] })); + + it('should be created', () => { + const service: LocalPermissionsService = TestBed.get(LocalPermissionsService); + expect(service).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/motions/services/local-permissions.service.ts b/client/src/app/site/motions/services/local-permissions.service.ts new file mode 100644 index 000000000..db87c5ae7 --- /dev/null +++ b/client/src/app/site/motions/services/local-permissions.service.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@angular/core'; +import { OperatorService } from '../../../core/services/operator.service'; +import { ViewMotion } from '../models/view-motion'; +import { ConfigService } from '../../../core/services/config.service'; + + +@Injectable({ + providedIn: 'root' +}) +export class LocalPermissionsService { + + public configMinSupporters: number; + + public constructor( + private operator: OperatorService, + private configService: ConfigService, + ) { + // load config variables + this.configService.get('motions_min_supporters').subscribe( + (supporters: number): void => { + this.configMinSupporters = supporters; + } + ); + } + + /** + * Should determine if the user (Operator) has the + * correct permission to perform the given action. + * + * actions might be: + * - support + * + * @param action the action the user tries to perform + */ + public isAllowed(action: string, motion?: ViewMotion): boolean { + if (motion) { + switch (action) { + case 'support': + return ( + this.operator.hasPerms('motions.can_support') && + this.configMinSupporters > 0 && + motion.state.allow_support && + (motion.submitters.indexOf(this.operator.user) === -1) && + (motion.supporters.indexOf(this.operator.user) === -1)); + case 'unsupport': + return ( + motion.state.allow_support && + (motion.supporters.indexOf(this.operator.user) !== -1) + ); + default: + return false; + } + } + } +} diff --git a/client/src/app/site/motions/services/motion-repository.service.ts b/client/src/app/site/motions/services/motion-repository.service.ts index cd20a96af..ea29548b2 100644 --- a/client/src/app/site/motions/services/motion-repository.service.ts +++ b/client/src/app/site/motions/services/motion-repository.service.ts @@ -106,7 +106,7 @@ export class MotionRepositoryService extends BaseRepository * Creates a (real) motion with patched data and delegate it * to the {@link DataSendService} * - * @param update the form data containing the update values + * @param update the form data containing the updated values * @param viewMotion The View Motion. If not present, a new motion will be created * TODO: Remove the viewMotion and make it actually distignuishable from save() */ @@ -123,7 +123,7 @@ export class MotionRepositoryService extends BaseRepository * Creates a (real) motion with patched data and delegate it * to the {@link DataSendService} * - * @param update the form data containing the update values + * @param update the form data containing the updated values * @param viewMotion The View Motion. If not present, a new motion will be created */ public async update(update: Partial, viewMotion: ViewMotion): Promise { @@ -158,11 +158,23 @@ export class MotionRepositoryService extends BaseRepository * Set the recommenders state of a motion * * @param viewMotion target motion - * @param stateId the number that indicates the state + * @param recommendationId the number that indicates the recommendation */ - public async setRecommenderState(viewMotion: ViewMotion, stateId: number): Promise { + public async setRecommendation(viewMotion: ViewMotion, recommendationId: number): Promise { const restPath = `/rest/motions/motion/${viewMotion.id}/set_recommendation/`; - await this.httpService.put(restPath, { recommendation: stateId }); + await this.httpService.put(restPath, { recommendation: recommendationId }); + } + + /** + * Set the category of a motion + * + * @param viewMotion target motion + * @param categoryId the number that indicates the category + */ + public async setCatetory(viewMotion: ViewMotion, categoryId: number): Promise { + const motion = viewMotion.motion; + motion.category_id = categoryId; + await this.update(motion, viewMotion); } /** @@ -175,6 +187,26 @@ export class MotionRepositoryService extends BaseRepository await this.httpService.post(url, data); } + /** + * Supports the motion + * + * @param viewMotion target motion + */ + public async support(viewMotion: ViewMotion): Promise { + const url = `/rest/motions/motion/${viewMotion.id}/support/`; + await this.httpService.post(url); + } + + /** + * Unsupports the motion + * + * @param viewMotion target motion + */ + public async unsupport(viewMotion: ViewMotion): Promise { + const url = `/rest/motions/motion/${viewMotion.id}/support/`; + await this.httpService.delete(url); + } + /** * Format the motion text using the line numbering and change * reco algorithm. diff --git a/client/src/styles.scss b/client/src/styles.scss index 111630f42..feb51201a 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -41,6 +41,10 @@ body { padding: 0; } +.small { + font-size: 90%; +} + .generic-mini-button { bottom: -28px; z-index: 100; @@ -114,6 +118,9 @@ body { cursor: pointer; background-color: rgba(0, 0, 0, 0.055); } + mat-row.lg { + height: 90px; + } } .card-plus-distance { @@ -157,7 +164,6 @@ mat-panel-title mat-icon { padding-right: 30px; } - .hidden-cell { flex: 0; width: 0; @@ -188,3 +194,59 @@ mat-panel-title mat-icon { display: none; } } + +.mat-chip, +.mat-basic-chip { + font-size: 12px; + min-height: 22px !important; + border-radius: 5px !important; + padding: 4px 8px !important; + margin: 8px 8px 8px 0; +} + +.mat-chip:focus, +.mat-basic-chip:focus { + outline: none; +} +button.mat-menu-item.selected { + font-weight: bold !important; +} + +/** Colors **/ +.lightblue { + background-color: rgb(33, 150, 243) !important; + color: white !important; +} + +.darkblue { + background-color: rgb(63, 81, 181) !important; + color: white !important; +} + +.green, +.success { + background-color: rgb(76, 175, 80) !important; + color: white !important; +} + +.red, +.error { + background-color: rgb(255, 82, 82) !important; + color: white !important; +} + +.yellow, +.warning { + background-color: rgb(255, 193, 7) !important; + color: white !important; +} + +.bluegrey { + background-color: rgb(96, 125, 139) !important; + color: white !important; +} + +.grey { + background-color: #e0e0e0 !important; + color: rgba(0, 0, 0, 0.87) !important; +}