2019-01-18 20:25:06 +01:00
|
|
|
import { Injectable } from '@angular/core';
|
|
|
|
|
|
|
|
import { TranslateService } from '@ngx-translate/core';
|
|
|
|
|
2019-01-19 21:55:06 +01:00
|
|
|
import { ChangeRecommendationRepositoryService } from 'app/core/repositories/motions/change-recommendation-repository.service';
|
2019-07-26 11:46:59 +02:00
|
|
|
import { MotionCommentSectionRepositoryService } from 'app/core/repositories/motions/motion-comment-section-repository.service';
|
2019-02-06 11:52:04 +01:00
|
|
|
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
|
2019-02-08 10:39:06 +01:00
|
|
|
import { StatuteParagraphRepositoryService } from 'app/core/repositories/motions/statute-paragraph-repository.service';
|
2019-07-26 11:46:59 +02:00
|
|
|
import { ConfigService } from 'app/core/ui-services/config.service';
|
|
|
|
import { HtmlToPdfService } from 'app/core/ui-services/html-to-pdf.service';
|
2019-02-11 15:45:23 +01:00
|
|
|
import { LinenumberingService } from 'app/core/ui-services/linenumbering.service';
|
2019-04-26 16:23:48 +02:00
|
|
|
import { PdfDocumentService } from 'app/core/ui-services/pdf-document.service';
|
2019-07-26 11:46:59 +02:00
|
|
|
import { CalculablePollKey } from 'app/core/ui-services/poll.service';
|
2019-05-09 12:52:10 +02:00
|
|
|
import { ViewUnifiedChange, ViewUnifiedChangeType } from 'app/shared/models/motions/view-unified-change';
|
2019-07-26 11:46:59 +02:00
|
|
|
import { getRecommendationTypeName } from 'app/shared/utils/recommendation-type-names';
|
2019-07-03 16:42:08 +02:00
|
|
|
import { ExportFormData } from '../modules/motion-list/components/motion-export-dialog/motion-export-dialog.component';
|
2019-07-26 11:46:59 +02:00
|
|
|
import { MotionPollService } from './motion-poll.service';
|
|
|
|
import { ChangeRecoMode, LineNumberingMode, ViewMotion } from '../models/view-motion';
|
|
|
|
import { ViewMotionAmendedParagraph } from '../models/view-motion-amended-paragraph';
|
|
|
|
import { ViewMotionChangeRecommendation } from '../models/view-motion-change-recommendation';
|
2019-02-06 13:13:19 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Type declaring which strings are valid options for metainfos to be exported into a pdf
|
|
|
|
*/
|
2019-02-11 17:11:35 +01:00
|
|
|
export type InfoToExport =
|
|
|
|
| 'submitters'
|
|
|
|
| 'state'
|
|
|
|
| 'recommendation'
|
|
|
|
| 'category'
|
2019-04-17 12:10:56 +02:00
|
|
|
| 'motion_block'
|
2019-02-11 17:11:35 +01:00
|
|
|
| 'origin'
|
2019-02-20 10:11:26 +01:00
|
|
|
| 'tags'
|
2019-02-11 17:11:35 +01:00
|
|
|
| 'polls'
|
|
|
|
| 'id'
|
|
|
|
| 'allcomments';
|
2019-02-06 13:13:19 +01:00
|
|
|
|
2019-01-18 20:25:06 +01:00
|
|
|
/**
|
|
|
|
* Converts a motion to pdf. Can be used from the motion detail view or executed on a list of motions
|
|
|
|
* Provides the public method `motionToDocDef(motion: Motion)` which should be convenient to use.
|
|
|
|
* `motionToDocDef(... )` accepts line numbering mode and change recommendation mode as optional parameter.
|
|
|
|
* If not present, the default parameters will be read from the config.
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* ```ts
|
|
|
|
* const pdfMakeCompatibleDocDef = this.MotionPdfService.motionToDocDef(myMotion);
|
|
|
|
* ```
|
|
|
|
*/
|
|
|
|
@Injectable({
|
|
|
|
providedIn: 'root'
|
|
|
|
})
|
|
|
|
export class MotionPdfService {
|
2019-05-09 12:52:10 +02:00
|
|
|
/**
|
|
|
|
* Get the {@link getRecommendationTypeName}-Function from Utils
|
|
|
|
*/
|
|
|
|
public getRecommendationTypeName = getRecommendationTypeName;
|
|
|
|
|
2019-01-18 20:25:06 +01:00
|
|
|
/**
|
|
|
|
* Constructor
|
|
|
|
*
|
|
|
|
* @param translate handle translations
|
|
|
|
* @param motionRepo get parent motions
|
2019-02-25 14:36:04 +01:00
|
|
|
* @param statuteRepo To get formated stature paragraphs
|
2019-01-18 20:25:06 +01:00
|
|
|
* @param changeRecoRepo to get the change recommendations
|
|
|
|
* @param configService Read config variables
|
2019-04-26 16:23:48 +02:00
|
|
|
* @param pdfDocumentService Global PDF Functions
|
2019-01-18 20:25:06 +01:00
|
|
|
* @param htmlToPdfService To convert HTML text into pdfmake doc def
|
2019-02-06 11:52:04 +01:00
|
|
|
* @param pollService MotionPollService for rendering the polls
|
2019-02-11 15:45:23 +01:00
|
|
|
* @param linenumberingService Line numbers
|
2019-02-11 17:11:35 +01:00
|
|
|
* @param commentRepo MotionCommentSectionRepositoryService to print comments
|
2019-01-18 20:25:06 +01:00
|
|
|
*/
|
|
|
|
public constructor(
|
|
|
|
private translate: TranslateService,
|
|
|
|
private motionRepo: MotionRepositoryService,
|
2019-02-25 14:36:04 +01:00
|
|
|
private statuteRepo: StatuteParagraphRepositoryService,
|
2019-01-18 20:25:06 +01:00
|
|
|
private changeRecoRepo: ChangeRecommendationRepositoryService,
|
|
|
|
private configService: ConfigService,
|
2019-04-26 16:23:48 +02:00
|
|
|
private pdfDocumentService: PdfDocumentService,
|
2019-02-06 11:52:04 +01:00
|
|
|
private htmlToPdfService: HtmlToPdfService,
|
2019-02-11 15:45:23 +01:00
|
|
|
private pollService: MotionPollService,
|
2019-02-11 17:11:35 +01:00
|
|
|
private linenumberingService: LinenumberingService,
|
|
|
|
private commentRepo: MotionCommentSectionRepositoryService
|
2019-01-18 20:25:06 +01:00
|
|
|
) {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Converts a motion to PdfMake doc definition
|
|
|
|
*
|
|
|
|
* @param motion the motion to convert to pdf
|
|
|
|
* @param lnMode determine the used line mode
|
|
|
|
* @param crMode determine the used change Recommendation mode
|
2019-01-25 17:03:05 +01:00
|
|
|
* @param contentToExport determine which content is to export. If left out, everything will be exported
|
|
|
|
* @param infoToExport determine which metaInfo to export. If left out, everything will be exported.
|
2019-02-11 17:11:35 +01:00
|
|
|
* @param commentsToExport comments to chose for export. If 'allcomments' is set in infoToExport, this selection will be ignored and all comments exported
|
2019-01-18 20:25:06 +01:00
|
|
|
* @returns doc def for the motion
|
|
|
|
*/
|
2019-07-03 16:42:08 +02:00
|
|
|
public motionToDocDef(motion: ViewMotion, exportInfo?: ExportFormData): object {
|
|
|
|
let lnMode = exportInfo && exportInfo.lnMode ? exportInfo.lnMode : null;
|
|
|
|
let crMode = exportInfo && exportInfo.crMode ? exportInfo.crMode : null;
|
|
|
|
const infoToExport = exportInfo ? exportInfo.metaInfo : null;
|
|
|
|
const contentToExport = exportInfo ? exportInfo.content : null;
|
|
|
|
let commentsToExport = exportInfo ? exportInfo.comments : null;
|
|
|
|
|
2019-05-09 12:52:10 +02:00
|
|
|
// get the line length from the config
|
|
|
|
const lineLength = this.configService.instant<number>('motions_line_length');
|
2019-06-11 17:55:56 +02:00
|
|
|
// whether to append checkboxes to follow the recommendation or not
|
|
|
|
const optionToFollowRecommendation = this.configService.instant<boolean>(
|
|
|
|
'motions_export_follow_recommendation'
|
|
|
|
);
|
|
|
|
|
2019-01-25 17:03:05 +01:00
|
|
|
let motionPdfContent = [];
|
|
|
|
|
2019-04-26 15:34:27 +02:00
|
|
|
// Enforces that statutes should always have Diff Mode and no line numbers
|
|
|
|
if (motion.isStatuteAmendment()) {
|
|
|
|
lnMode = LineNumberingMode.None;
|
|
|
|
crMode = ChangeRecoMode.Diff;
|
|
|
|
}
|
|
|
|
|
2019-01-18 20:25:06 +01:00
|
|
|
// determine the default lnMode if not explicitly given
|
|
|
|
if (!lnMode) {
|
|
|
|
lnMode = this.configService.instant('motions_default_line_numbering');
|
|
|
|
}
|
|
|
|
|
|
|
|
// determine the default crMode if not explicitly given
|
|
|
|
if (!crMode) {
|
2019-01-24 14:40:05 +01:00
|
|
|
crMode = this.configService.instant('motions_recommendation_text_mode');
|
2019-01-18 20:25:06 +01:00
|
|
|
}
|
|
|
|
|
2019-06-30 09:30:11 +02:00
|
|
|
const title = this.createTitle(motion, crMode, lineLength);
|
2019-02-11 17:11:35 +01:00
|
|
|
const sequential = !infoToExport || infoToExport.includes('id');
|
|
|
|
const subtitle = this.createSubtitle(motion, sequential);
|
2019-01-18 20:25:06 +01:00
|
|
|
|
2019-01-25 17:03:05 +01:00
|
|
|
motionPdfContent = [title, subtitle];
|
|
|
|
|
|
|
|
if ((infoToExport && infoToExport.length > 0) || !infoToExport) {
|
2019-06-11 17:55:56 +02:00
|
|
|
const metaInfo = this.createMetaInfoTable(
|
|
|
|
motion,
|
|
|
|
lineLength,
|
|
|
|
crMode,
|
|
|
|
infoToExport,
|
|
|
|
optionToFollowRecommendation
|
|
|
|
);
|
2019-01-25 17:03:05 +01:00
|
|
|
motionPdfContent.push(metaInfo);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!contentToExport || contentToExport.includes('text')) {
|
|
|
|
const preamble = this.createPreamble(motion);
|
|
|
|
motionPdfContent.push(preamble);
|
2019-05-09 12:52:10 +02:00
|
|
|
const text = this.createText(motion, lineLength, lnMode, crMode);
|
2019-01-25 17:03:05 +01:00
|
|
|
motionPdfContent.push(text);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!contentToExport || contentToExport.includes('reason')) {
|
|
|
|
const reason = this.createReason(motion, lnMode);
|
|
|
|
motionPdfContent.push(reason);
|
|
|
|
}
|
|
|
|
|
2019-02-11 17:11:35 +01:00
|
|
|
if (infoToExport && infoToExport.includes('allcomments')) {
|
2019-06-28 07:24:28 +02:00
|
|
|
commentsToExport = this.commentRepo.getViewModelList().map(vm => vm.id);
|
2019-02-11 17:11:35 +01:00
|
|
|
}
|
|
|
|
if (commentsToExport) {
|
|
|
|
motionPdfContent.push(this.createComments(motion, commentsToExport));
|
|
|
|
}
|
|
|
|
|
2019-01-18 20:25:06 +01:00
|
|
|
return motionPdfContent;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create the motion title part of the doc definition
|
|
|
|
*
|
|
|
|
* @param motion the target motion
|
2019-06-30 09:30:11 +02:00
|
|
|
* @param crMode the change recommendation mode
|
|
|
|
* @param lineLength the line length
|
2019-01-18 20:25:06 +01:00
|
|
|
* @returns doc def for the document title
|
|
|
|
*/
|
2019-06-30 09:30:11 +02:00
|
|
|
private createTitle(motion: ViewMotion, crMode: ChangeRecoMode, lineLength: number): object {
|
|
|
|
// summary of change recommendations (for motion diff version only)
|
|
|
|
const changes = this.getUnifiedChanges(motion, lineLength);
|
|
|
|
const titleChange = changes.find(change => change.isTitleChange());
|
|
|
|
const changedTitle = this.changeRecoRepo.getTitleWithChanges(motion.title, titleChange, crMode);
|
|
|
|
|
2019-01-18 20:25:06 +01:00
|
|
|
const identifier = motion.identifier ? ' ' + motion.identifier : '';
|
2019-07-03 16:42:08 +02:00
|
|
|
const pageSize = this.configService.instant('general_export_pdf_pagesize');
|
|
|
|
let title = '';
|
|
|
|
if (pageSize === 'A4') {
|
|
|
|
title += `${this.translate.instant('Motion')} `;
|
|
|
|
}
|
|
|
|
|
|
|
|
title += `${identifier}: ${changedTitle}`;
|
2019-01-18 20:25:06 +01:00
|
|
|
|
|
|
|
return {
|
|
|
|
text: title,
|
|
|
|
style: 'title'
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create the motion subtitle and sequential number part of the doc definition
|
|
|
|
*
|
|
|
|
* @param motion the target motion
|
2019-02-11 17:11:35 +01:00
|
|
|
* @param sequential set to true to include the sequential number
|
2019-01-18 20:25:06 +01:00
|
|
|
* @returns doc def for the subtitle
|
|
|
|
*/
|
2019-02-11 17:11:35 +01:00
|
|
|
private createSubtitle(motion: ViewMotion, sequential?: boolean): object {
|
2019-01-18 20:25:06 +01:00
|
|
|
const subtitleLines = [];
|
2019-02-11 17:11:35 +01:00
|
|
|
if (sequential) {
|
2019-01-18 20:25:06 +01:00
|
|
|
subtitleLines.push(`${this.translate.instant('Sequential number')}: ${motion.id}`);
|
|
|
|
}
|
|
|
|
|
2019-02-01 14:27:24 +01:00
|
|
|
if (motion.parent_id) {
|
2019-02-11 17:11:35 +01:00
|
|
|
if (sequential) {
|
2019-02-01 14:27:24 +01:00
|
|
|
subtitleLines.push(' • ');
|
|
|
|
}
|
|
|
|
subtitleLines.push(
|
|
|
|
`${this.translate.instant('Amendment to')} ${motion.parent.identifier || motion.parent.title}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-01-18 20:25:06 +01:00
|
|
|
return {
|
|
|
|
text: subtitleLines,
|
|
|
|
style: 'subtitle'
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates the MetaInfoTable
|
|
|
|
*
|
|
|
|
* @param motion the target motion
|
|
|
|
* @returns doc def for the meta infos
|
|
|
|
*/
|
2019-05-09 12:52:10 +02:00
|
|
|
private createMetaInfoTable(
|
|
|
|
motion: ViewMotion,
|
|
|
|
lineLength: number,
|
|
|
|
crMode: ChangeRecoMode,
|
2019-06-11 17:55:56 +02:00
|
|
|
infoToExport?: InfoToExport[],
|
|
|
|
optionToFollowRecommendation?: boolean
|
2019-05-09 12:52:10 +02:00
|
|
|
): object {
|
2019-01-18 20:25:06 +01:00
|
|
|
const metaTableBody = [];
|
|
|
|
|
|
|
|
// submitters
|
2019-01-25 17:03:05 +01:00
|
|
|
if (!infoToExport || infoToExport.includes('submitters')) {
|
2019-07-17 16:13:49 +02:00
|
|
|
const submitters = motion.submittersAsUsers
|
|
|
|
.map(user => {
|
|
|
|
return user.full_name;
|
2019-01-25 17:03:05 +01:00
|
|
|
})
|
|
|
|
.join(', ');
|
|
|
|
|
|
|
|
metaTableBody.push([
|
|
|
|
{
|
|
|
|
text: `${this.translate.instant('Submitters')}:`,
|
|
|
|
style: 'boldText'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: submitters
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
}
|
2019-01-18 20:25:06 +01:00
|
|
|
|
2019-07-08 21:39:38 +02:00
|
|
|
// supporters
|
|
|
|
const minSupporters = this.configService.instant<number>('motions_min_supporters');
|
|
|
|
if (minSupporters && motion.supporters.length > 0) {
|
|
|
|
const supporters = motion.supporters
|
|
|
|
.map(supporter => {
|
|
|
|
return supporter.full_name;
|
|
|
|
})
|
|
|
|
.join(', ');
|
|
|
|
|
|
|
|
metaTableBody.push([
|
|
|
|
{
|
|
|
|
text: `${this.translate.instant('Supporters')}:`,
|
|
|
|
style: 'boldText'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: supporters
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2019-01-18 20:25:06 +01:00
|
|
|
// state
|
2019-01-25 17:03:05 +01:00
|
|
|
if (!infoToExport || infoToExport.includes('state')) {
|
|
|
|
metaTableBody.push([
|
|
|
|
{
|
|
|
|
text: `${this.translate.instant('State')}:`,
|
|
|
|
style: 'boldText'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: this.motionRepo.getExtendedStateLabel(motion)
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
}
|
2019-01-18 20:25:06 +01:00
|
|
|
|
|
|
|
// recommendation
|
2019-01-25 17:03:05 +01:00
|
|
|
if (motion.recommendation && (!infoToExport || infoToExport.includes('recommendation'))) {
|
2019-01-31 10:13:47 +01:00
|
|
|
let recommendationByText: string;
|
|
|
|
|
|
|
|
if (motion.isStatuteAmendment()) {
|
|
|
|
recommendationByText = this.configService.instant('motions_statute_recommendations_by');
|
|
|
|
} else {
|
|
|
|
recommendationByText = this.configService.instant('motions_recommendations_by');
|
|
|
|
}
|
|
|
|
|
2019-01-18 20:25:06 +01:00
|
|
|
metaTableBody.push([
|
|
|
|
{
|
2019-01-31 10:13:47 +01:00
|
|
|
text: `${recommendationByText}:`,
|
2019-01-18 20:25:06 +01:00
|
|
|
style: 'boldText'
|
|
|
|
},
|
|
|
|
{
|
2019-01-21 16:34:19 +01:00
|
|
|
text: this.motionRepo.getExtendedRecommendationLabel(motion)
|
2019-01-18 20:25:06 +01:00
|
|
|
}
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// category
|
2019-01-25 17:03:05 +01:00
|
|
|
if (motion.category && (!infoToExport || infoToExport.includes('category'))) {
|
2019-07-16 10:48:48 +02:00
|
|
|
let categoryText = '';
|
|
|
|
if (!!motion.category.parent) {
|
|
|
|
categoryText = `${motion.category.parent.toString()}\n${this.translate.instant(
|
|
|
|
'Subcategory'
|
|
|
|
)}: ${motion.category.toString()}`;
|
|
|
|
} else {
|
|
|
|
categoryText = motion.category.toString();
|
|
|
|
}
|
2019-01-18 20:25:06 +01:00
|
|
|
metaTableBody.push([
|
|
|
|
{
|
|
|
|
text: `${this.translate.instant('Category')}:`,
|
|
|
|
style: 'boldText'
|
|
|
|
},
|
|
|
|
{
|
2019-07-16 10:48:48 +02:00
|
|
|
text: categoryText
|
2019-01-18 20:25:06 +01:00
|
|
|
}
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2019-02-20 10:11:26 +01:00
|
|
|
// tags
|
|
|
|
if (motion.tags.length && (!infoToExport || infoToExport.includes('tags'))) {
|
|
|
|
const tags = motion.tags
|
|
|
|
.map(tag => {
|
|
|
|
return tag;
|
|
|
|
})
|
|
|
|
.join(', ');
|
|
|
|
|
|
|
|
metaTableBody.push([
|
|
|
|
{
|
|
|
|
text: `${this.translate.instant('Tags')}:`,
|
|
|
|
style: 'boldText'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: tags
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2019-01-18 20:25:06 +01:00
|
|
|
// motion block
|
2019-04-17 12:10:56 +02:00
|
|
|
if (motion.motion_block && (!infoToExport || infoToExport.includes('motion_block'))) {
|
2019-01-18 20:25:06 +01:00
|
|
|
metaTableBody.push([
|
|
|
|
{
|
|
|
|
text: `${this.translate.instant('Motion block')}:`,
|
|
|
|
style: 'boldText'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: motion.motion_block.title
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// origin
|
2019-01-25 17:03:05 +01:00
|
|
|
if (motion.origin && (!infoToExport || infoToExport.includes('origin'))) {
|
2019-01-18 20:25:06 +01:00
|
|
|
metaTableBody.push([
|
|
|
|
{
|
|
|
|
text: `${this.translate.instant('Origin')}:`,
|
2019-01-25 17:03:05 +01:00
|
|
|
style: 'boldText'
|
2019-01-18 20:25:06 +01:00
|
|
|
},
|
|
|
|
{
|
|
|
|
text: motion.origin
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2019-02-11 17:11:35 +01:00
|
|
|
// voting results
|
2019-02-06 11:52:04 +01:00
|
|
|
if (motion.motion.polls.length && (!infoToExport || infoToExport.includes('polls'))) {
|
|
|
|
const column1 = [];
|
|
|
|
const column2 = [];
|
|
|
|
const column3 = [];
|
|
|
|
motion.motion.polls.map((poll, index) => {
|
|
|
|
if (poll.has_votes) {
|
|
|
|
if (motion.motion.polls.length > 1) {
|
|
|
|
column1.push(index + 1 + '. ' + this.translate.instant('Vote'));
|
|
|
|
column2.push('');
|
|
|
|
column3.push('');
|
|
|
|
}
|
|
|
|
const values: CalculablePollKey[] = ['yes', 'no', 'abstain'];
|
|
|
|
if (poll.votesvalid) {
|
|
|
|
values.push('votesvalid');
|
|
|
|
}
|
|
|
|
if (poll.votesinvalid) {
|
|
|
|
values.push('votesinvalid');
|
|
|
|
}
|
|
|
|
if (poll.votescast) {
|
|
|
|
values.push('votescast');
|
|
|
|
}
|
|
|
|
values.map(value => {
|
|
|
|
column1.push(`${this.translate.instant(this.pollService.getLabel(value))}:`);
|
|
|
|
column2.push(`${this.translate.instant(this.pollService.getSpecialLabel(poll[value]))}`);
|
|
|
|
this.pollService.isAbstractValue(poll, value)
|
|
|
|
? column3.push('')
|
|
|
|
: column3.push(`(${this.pollService.calculatePercentage(poll, value)} %)`);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
metaTableBody.push([
|
|
|
|
{
|
|
|
|
text: `${this.translate.instant('Voting result')}:`,
|
|
|
|
style: 'boldText'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
columns: [
|
|
|
|
{
|
|
|
|
text: column1.join('\n'),
|
|
|
|
width: 'auto'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: column2.join('\n'),
|
|
|
|
width: 'auto',
|
|
|
|
alignment: 'right'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: column3.join('\n'),
|
|
|
|
width: 'auto',
|
|
|
|
alignment: 'right'
|
|
|
|
}
|
|
|
|
],
|
|
|
|
columnGap: 7
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
}
|
2019-01-18 20:25:06 +01:00
|
|
|
|
|
|
|
// summary of change recommendations (for motion diff version only)
|
2019-05-09 12:52:10 +02:00
|
|
|
const changes = this.getUnifiedChanges(motion, lineLength);
|
2019-02-14 15:27:23 +01:00
|
|
|
|
2019-05-09 12:52:10 +02:00
|
|
|
if (crMode === ChangeRecoMode.Diff && changes.length > 0) {
|
2019-01-18 20:25:06 +01:00
|
|
|
const columnLineNumbers = [];
|
|
|
|
const columnChangeType = [];
|
|
|
|
|
2019-05-09 12:52:10 +02:00
|
|
|
changes.forEach(change => {
|
2019-06-30 09:30:11 +02:00
|
|
|
if (change.isTitleChange()) {
|
|
|
|
// Is always a change recommendation
|
2019-05-09 12:52:10 +02:00
|
|
|
const changeReco = change as ViewMotionChangeRecommendation;
|
2019-06-30 09:30:11 +02:00
|
|
|
columnLineNumbers.push(`${this.translate.instant('Title')}: `);
|
2019-05-09 12:52:10 +02:00
|
|
|
columnChangeType.push(
|
|
|
|
`(${this.translate.instant('Change recommendation')}) - ${this.translate.instant(
|
|
|
|
this.getRecommendationTypeName(changeReco)
|
|
|
|
)}`
|
|
|
|
);
|
2019-06-30 09:30:11 +02:00
|
|
|
} else {
|
|
|
|
// line numbers column
|
|
|
|
let line;
|
|
|
|
if (change.getLineFrom() >= change.getLineTo() - 1) {
|
|
|
|
line = change.getLineFrom();
|
|
|
|
} else {
|
|
|
|
line = change.getLineFrom() + ' - ' + (change.getLineTo() - 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// change type column
|
|
|
|
if (change.getChangeType() === ViewUnifiedChangeType.TYPE_CHANGE_RECOMMENDATION) {
|
|
|
|
const changeReco = change as ViewMotionChangeRecommendation;
|
2019-05-09 12:52:10 +02:00
|
|
|
columnLineNumbers.push(`${this.translate.instant('Line')} ${line}: `);
|
2019-06-30 09:30:11 +02:00
|
|
|
columnChangeType.push(
|
|
|
|
`(${this.translate.instant('Change recommendation')}) - ${this.translate.instant(
|
|
|
|
this.getRecommendationTypeName(changeReco)
|
|
|
|
)}`
|
|
|
|
);
|
|
|
|
} else if (change.getChangeType() === ViewUnifiedChangeType.TYPE_AMENDMENT) {
|
|
|
|
const amendment = change as ViewMotionAmendedParagraph;
|
|
|
|
let summaryText = `(${this.translate.instant('Amendment')} ${amendment.getIdentifier()}) -`;
|
|
|
|
if (amendment.isRejected()) {
|
|
|
|
summaryText += ` ${this.translate.instant('Rejected')}`;
|
|
|
|
} else if (amendment.isAccepted()) {
|
|
|
|
summaryText += ` ${this.translate.instant(amendment.stateName)}`;
|
|
|
|
// only append line and change, if the merge of the state of the amendment is accepted.
|
|
|
|
columnLineNumbers.push(`${this.translate.instant('Line')} ${line}: `);
|
|
|
|
columnChangeType.push(summaryText);
|
|
|
|
}
|
2019-05-09 12:52:10 +02:00
|
|
|
}
|
2019-01-18 20:25:06 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2019-05-09 12:52:10 +02:00
|
|
|
if (columnChangeType.length > 0) {
|
|
|
|
metaTableBody.push([
|
|
|
|
{
|
2019-06-11 17:55:56 +02:00
|
|
|
text: this.translate.instant('Summary of changes:'),
|
2019-05-09 12:52:10 +02:00
|
|
|
style: 'boldText'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
columns: [
|
|
|
|
{
|
|
|
|
text: columnLineNumbers.join('\n'),
|
|
|
|
width: 'auto'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: columnChangeType.join('\n'),
|
|
|
|
width: 'auto'
|
|
|
|
}
|
|
|
|
],
|
|
|
|
columnGap: 7
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
}
|
2019-01-18 20:25:06 +01:00
|
|
|
}
|
|
|
|
|
2019-06-11 17:55:56 +02:00
|
|
|
// Checkboxes for resolution
|
|
|
|
if (optionToFollowRecommendation) {
|
|
|
|
metaTableBody.push([
|
|
|
|
{
|
|
|
|
text: `${this.translate.instant('Decision')}:`,
|
|
|
|
style: 'boldText'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
margin: [5, 2, 0, 2],
|
|
|
|
columns: [
|
|
|
|
{
|
|
|
|
width: 8,
|
|
|
|
canvas: this.pdfDocumentService.drawCircle(6.5, 4)
|
|
|
|
},
|
|
|
|
{
|
|
|
|
width: 'auto',
|
|
|
|
text: this.translate.instant('As recommendation')
|
|
|
|
},
|
|
|
|
{
|
|
|
|
width: 20,
|
|
|
|
text: ''
|
|
|
|
},
|
|
|
|
{
|
|
|
|
width: 8,
|
|
|
|
canvas: this.pdfDocumentService.drawCircle(6.5, 4)
|
|
|
|
},
|
|
|
|
{
|
|
|
|
width: 'auto',
|
|
|
|
text: this.translate.instant('Divergent:')
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2019-01-25 17:03:05 +01:00
|
|
|
if (metaTableBody.length > 0) {
|
|
|
|
return {
|
|
|
|
table: {
|
|
|
|
widths: ['35%', '65%'],
|
|
|
|
body: metaTableBody
|
2019-01-18 20:25:06 +01:00
|
|
|
},
|
2019-01-25 17:03:05 +01:00
|
|
|
margin: [0, 0, 0, 20],
|
|
|
|
// That did not work too well in the past. Perhaps substitution by a pdfWorker the worker will be necessary
|
|
|
|
layout: {
|
|
|
|
fillColor: () => {
|
|
|
|
return '#dddddd';
|
|
|
|
},
|
|
|
|
hLineWidth: (i, node) => {
|
|
|
|
return i === 0 || i === node.table.body.length ? 0 : 0.5;
|
|
|
|
},
|
|
|
|
vLineWidth: () => {
|
|
|
|
return 0;
|
|
|
|
},
|
|
|
|
hLineColor: () => {
|
|
|
|
return 'white';
|
|
|
|
}
|
2019-01-18 20:25:06 +01:00
|
|
|
}
|
2019-01-25 17:03:05 +01:00
|
|
|
};
|
|
|
|
}
|
2019-01-18 20:25:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates the motion preamble
|
|
|
|
*
|
|
|
|
* @param motion the target motion
|
|
|
|
* @returns doc def for the motion text
|
|
|
|
*/
|
|
|
|
private createPreamble(motion: ViewMotion): object {
|
|
|
|
return {
|
|
|
|
text: `${this.translate.instant(this.configService.instant('motions_preamble'))}`,
|
|
|
|
margin: [0, 10, 0, 10]
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates the motion text - uses HTML to PDF
|
|
|
|
*
|
|
|
|
* @param motion the motion to convert to pdf
|
2019-06-30 09:30:11 +02:00
|
|
|
* @param lineLength the current line length
|
2019-01-18 20:25:06 +01:00
|
|
|
* @param lnMode determine the used line mode
|
|
|
|
* @param crMode determine the used change Recommendation mode
|
|
|
|
* @returns doc def for the "the assembly may decide" preamble
|
|
|
|
*/
|
2019-05-09 12:52:10 +02:00
|
|
|
private createText(
|
|
|
|
motion: ViewMotion,
|
|
|
|
lineLength: number,
|
|
|
|
lnMode: LineNumberingMode,
|
|
|
|
crMode: ChangeRecoMode
|
|
|
|
): object {
|
2019-06-30 09:30:11 +02:00
|
|
|
let motionText = '';
|
2019-02-08 10:39:06 +01:00
|
|
|
|
2019-01-18 20:25:06 +01:00
|
|
|
if (motion.isParagraphBasedAmendment()) {
|
2019-02-15 13:42:01 +01:00
|
|
|
// this is logically redundant with the formation of amendments in the motion-detail html.
|
|
|
|
// Should be refactored in a way that a service returns the correct html for both cases
|
2019-06-29 09:59:39 +02:00
|
|
|
for (const paragraph of this.motionRepo.getAmendmentParagraphs(motion, lineLength, false)) {
|
2019-02-15 13:42:01 +01:00
|
|
|
if (paragraph.diffLineTo === paragraph.diffLineFrom + 1) {
|
|
|
|
motionText += `<h3>
|
|
|
|
${this.translate.instant('Line')} ${paragraph.diffLineFrom}:
|
|
|
|
</h3>`;
|
|
|
|
} else {
|
|
|
|
motionText += `<h3>
|
|
|
|
${this.translate.instant('Line')} ${paragraph.diffLineFrom} - ${paragraph.diffLineTo - 1}:
|
|
|
|
</h3>`;
|
|
|
|
}
|
|
|
|
|
|
|
|
motionText += `<div class="paragraphcontext">${paragraph.textPre}</div>`;
|
|
|
|
motionText += paragraph.text;
|
|
|
|
motionText += `<div class="paragraphcontext">${paragraph.textPost}</div>`;
|
|
|
|
}
|
2019-02-08 10:39:06 +01:00
|
|
|
} else if (motion.isStatuteAmendment()) {
|
|
|
|
// statute amendments
|
2019-02-25 14:36:04 +01:00
|
|
|
const statutes = this.statuteRepo.getViewModelList();
|
2019-02-08 10:39:06 +01:00
|
|
|
motionText = this.motionRepo.formatStatuteAmendment(statutes, motion, lineLength);
|
2019-01-18 20:25:06 +01:00
|
|
|
} else {
|
|
|
|
// lead motion or normal amendments
|
2019-04-04 13:22:12 +02:00
|
|
|
|
2019-05-09 12:52:10 +02:00
|
|
|
const changes = this.getUnifiedChanges(motion, lineLength);
|
2019-06-30 09:30:11 +02:00
|
|
|
const textChanges = changes.filter(change => !change.isTitleChange());
|
|
|
|
const titleChange = changes.find(change => change.isTitleChange());
|
|
|
|
|
|
|
|
if (crMode === ChangeRecoMode.Diff && titleChange) {
|
|
|
|
const changedTitle = this.changeRecoRepo.getTitleChangesAsDiff(motion.title, titleChange);
|
|
|
|
motionText +=
|
|
|
|
'<span><strong>' +
|
|
|
|
this.translate.instant('Changed title') +
|
|
|
|
':</strong><br>' +
|
|
|
|
changedTitle +
|
|
|
|
'</span><br>';
|
|
|
|
}
|
|
|
|
const formattedText = this.motionRepo.formatMotion(motion.id, crMode, textChanges, lineLength);
|
2019-02-11 15:45:23 +01:00
|
|
|
// reformat motion text to split long HTML elements to easier convert into PDF
|
2019-06-30 09:30:11 +02:00
|
|
|
motionText += this.linenumberingService.splitInlineElementsAtLineBreaks(formattedText);
|
2019-01-18 20:25:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return this.htmlToPdfService.convertHtml(motionText, lnMode);
|
|
|
|
}
|
|
|
|
|
2019-05-09 12:52:10 +02:00
|
|
|
/**
|
|
|
|
* changes need to be sorted, by "line from".
|
|
|
|
* otherwise, formatMotion will make unexpected results by messing up the
|
|
|
|
* order of changes applied to the motion
|
|
|
|
*
|
|
|
|
* TODO: Cleanup, everything change reco and amendment based needs a unified structure.
|
|
|
|
*
|
|
|
|
* @param motion
|
|
|
|
* @param lineLength
|
|
|
|
* @returns
|
|
|
|
*/
|
|
|
|
private getUnifiedChanges(motion: ViewMotion, lineLength: number): ViewUnifiedChange[] {
|
|
|
|
return this.changeRecoRepo
|
|
|
|
.getChangeRecoOfMotion(motion.id)
|
|
|
|
.concat(
|
|
|
|
this.motionRepo
|
|
|
|
.getAmendmentsInstantly(motion.id)
|
|
|
|
.flatMap((amendment: ViewMotion) =>
|
|
|
|
this.motionRepo.getAmendmentAmendedParagraphs(amendment, lineLength)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
.sort((a, b) => a.getLineFrom() - b.getLineFrom()) as ViewUnifiedChange[];
|
|
|
|
}
|
|
|
|
|
2019-01-18 20:25:06 +01:00
|
|
|
/**
|
|
|
|
* Creates the motion reason - uses HTML to PDF
|
|
|
|
*
|
|
|
|
* @param motion the target motion
|
|
|
|
* @returns doc def for the reason as array
|
|
|
|
*/
|
|
|
|
private createReason(motion: ViewMotion, lnMode: LineNumberingMode): object {
|
|
|
|
if (motion.reason) {
|
|
|
|
const reason = [];
|
|
|
|
|
|
|
|
// add the reason "head line"
|
|
|
|
reason.push({
|
|
|
|
text: this.translate.instant('Reason'),
|
|
|
|
style: 'heading3',
|
|
|
|
margin: [0, 25, 0, 10]
|
|
|
|
});
|
|
|
|
|
|
|
|
// determine the width of the reason depending on line numbering
|
|
|
|
// currently not used
|
|
|
|
// let columnWidth: string;
|
|
|
|
// if (lnMode === LineNumberingMode.Outside) {
|
|
|
|
// columnWidth = '80%';
|
|
|
|
// } else {
|
|
|
|
// columnWidth = '100%';
|
|
|
|
// }
|
|
|
|
|
2019-05-13 18:19:29 +02:00
|
|
|
reason.push(this.htmlToPdfService.addPlainText(motion.reason));
|
2019-01-18 20:25:06 +01:00
|
|
|
|
|
|
|
return reason;
|
|
|
|
} else {
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
}
|
2019-01-31 19:18:58 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates pdfMake definitions for the call list of given motions
|
2019-02-12 17:59:33 +01:00
|
|
|
* Any motions that are 'top level' (no sort_parent_id) will have their tags
|
|
|
|
* used as comma separated header titles in an extra row
|
2019-01-31 19:18:58 +01:00
|
|
|
*
|
|
|
|
* @param motions A list of motions
|
|
|
|
* @returns definitions ready to be opened or exported via {@link PdfDocumentService}
|
|
|
|
*/
|
|
|
|
public callListToDoc(motions: ViewMotion[]): object {
|
2019-05-13 15:50:27 +02:00
|
|
|
motions.sort((a, b) => a.weight - b.weight);
|
2019-01-31 19:18:58 +01:00
|
|
|
const title = {
|
|
|
|
text: this.translate.instant('Call list'),
|
|
|
|
style: 'title'
|
|
|
|
};
|
|
|
|
const callListTableBody: object[] = [
|
|
|
|
[
|
|
|
|
{
|
|
|
|
text: this.translate.instant('Called'),
|
|
|
|
style: 'tableHeader'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: this.translate.instant('Called with'),
|
|
|
|
style: 'tableHeader'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: this.translate.instant('Submitters'),
|
|
|
|
style: 'tableHeader'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: this.translate.instant('Title'),
|
|
|
|
style: 'tableHeader'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: this.translate.instant('Recommendation'),
|
|
|
|
style: 'tableHeader'
|
|
|
|
},
|
|
|
|
{
|
2019-04-12 12:28:18 +02:00
|
|
|
text: this.translate.instant('Notes'),
|
2019-01-31 19:18:58 +01:00
|
|
|
style: 'tableHeader'
|
|
|
|
}
|
|
|
|
]
|
|
|
|
];
|
|
|
|
|
2019-02-12 17:59:33 +01:00
|
|
|
const callListRows: object[] = [];
|
|
|
|
let currentTitle = '';
|
|
|
|
|
2019-02-14 10:07:42 +01:00
|
|
|
motions.forEach(motion => {
|
2019-02-12 17:59:33 +01:00
|
|
|
if (!motion.sort_parent_id) {
|
|
|
|
const heading = motion.tags ? motion.tags.map(tag => tag.name).join(', ') : '';
|
|
|
|
if (heading !== currentTitle) {
|
|
|
|
callListRows.push([
|
|
|
|
{
|
|
|
|
text: heading,
|
|
|
|
colSpan: 6,
|
|
|
|
style: 'heading3',
|
|
|
|
margin: [0, 10, 0, 10]
|
|
|
|
},
|
|
|
|
'',
|
|
|
|
'',
|
|
|
|
'',
|
|
|
|
'',
|
|
|
|
''
|
|
|
|
]);
|
|
|
|
currentTitle = heading;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
callListRows.push(this.createCallListRow(motion));
|
|
|
|
});
|
|
|
|
|
2019-01-31 19:18:58 +01:00
|
|
|
const table: object = {
|
|
|
|
table: {
|
|
|
|
widths: ['auto', 'auto', 'auto', '*', 'auto', 'auto'],
|
|
|
|
headerRows: 1,
|
2019-04-12 12:28:18 +02:00
|
|
|
dontBreakRows: true,
|
2019-01-31 19:18:58 +01:00
|
|
|
body: callListTableBody.concat(callListRows)
|
|
|
|
},
|
2019-04-26 16:23:48 +02:00
|
|
|
layout: this.pdfDocumentService.switchColorTableLayout
|
2019-01-31 19:18:58 +01:00
|
|
|
};
|
|
|
|
return [title, table];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates the pdfMake definitions for a row of the call List table
|
|
|
|
*
|
|
|
|
* @param motion
|
|
|
|
* @returns pdfmakre definitions
|
|
|
|
*/
|
|
|
|
private createCallListRow(motion: ViewMotion): object {
|
|
|
|
return [
|
|
|
|
{
|
|
|
|
text: motion.sort_parent_id ? '' : motion.identifierOrTitle
|
|
|
|
},
|
|
|
|
{ text: motion.sort_parent_id ? motion.identifierOrTitle : '' },
|
2019-07-17 16:13:49 +02:00
|
|
|
{ text: motion.submitters.length ? motion.submittersAsUsers.map(s => s.short_name).join(', ') : '' },
|
2019-01-31 19:18:58 +01:00
|
|
|
{ text: motion.title },
|
|
|
|
{
|
2019-04-12 12:28:18 +02:00
|
|
|
text: motion.recommendation ? this.motionRepo.getExtendedRecommendationLabel(motion) : ''
|
2019-01-31 19:18:58 +01:00
|
|
|
},
|
2019-04-12 12:28:18 +02:00
|
|
|
{ text: '' }
|
2019-01-31 19:18:58 +01:00
|
|
|
];
|
|
|
|
}
|
2019-02-06 13:13:19 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates pdfmake definitions for basic information about the motion and
|
|
|
|
* comments or notes
|
|
|
|
*
|
|
|
|
* @param note string optionally containing html layout
|
|
|
|
* @param motion the ViewMotion this note refers to
|
|
|
|
* @param noteTitle additional heading to be used (will be translated)
|
|
|
|
* @returns pdfMake definitions
|
|
|
|
*/
|
|
|
|
public textToDocDef(note: string, motion: ViewMotion, noteTitle: string): object {
|
2019-06-30 09:30:11 +02:00
|
|
|
const lineLength = this.configService.instant<number>('motions_line_length');
|
|
|
|
const crMode = this.configService.instant<ChangeRecoMode>('motions_recommendation_text_mode');
|
|
|
|
const title = this.createTitle(motion, crMode, lineLength);
|
2019-02-06 13:13:19 +01:00
|
|
|
const subtitle = this.createSubtitle(motion);
|
2019-06-30 09:30:11 +02:00
|
|
|
const metaInfo = this.createMetaInfoTable(motion, lineLength, crMode, ['submitters', 'state', 'category']);
|
2019-05-06 16:22:35 +02:00
|
|
|
const noteContent = this.htmlToPdfService.convertHtml(note, LineNumberingMode.None);
|
2019-02-06 13:13:19 +01:00
|
|
|
|
|
|
|
const subHeading = {
|
|
|
|
text: this.translate.instant(noteTitle),
|
|
|
|
style: 'heading2'
|
|
|
|
};
|
|
|
|
return [title, subtitle, metaInfo, subHeading, noteContent];
|
|
|
|
}
|
2019-02-11 17:11:35 +01:00
|
|
|
|
|
|
|
private createComments(motion: ViewMotion, comments: number[]): object[] {
|
|
|
|
const result: object[] = [];
|
|
|
|
for (const comment of comments) {
|
|
|
|
const viewComment = this.commentRepo.getViewModel(comment);
|
|
|
|
const section = motion.getCommentForSection(viewComment);
|
|
|
|
if (section && section.comment) {
|
2019-05-06 16:22:35 +02:00
|
|
|
result.push({ text: viewComment.name, style: 'heading3', margin: [0, 25, 0, 10] });
|
2019-05-13 18:19:29 +02:00
|
|
|
result.push(this.htmlToPdfService.addPlainText(section.comment));
|
2019-02-11 17:11:35 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
2019-01-18 20:25:06 +01:00
|
|
|
}
|