diff --git a/client/src/app/core/ui-services/pdf-document.service.ts b/client/src/app/core/ui-services/pdf-document.service.ts index 4c0a6baf0..beef2acb4 100644 --- a/client/src/app/core/ui-services/pdf-document.service.ts +++ b/client/src/app/core/ui-services/pdf-document.service.ts @@ -119,23 +119,36 @@ export class PdfDocumentService { * @param documentContent the content of the pdf as object * @param metadata * @param imageUrls Array of optional images (url, placeholder) to be inserted + * @param customMargins optionally overrides the margins + * @param landscape optional landscape page orientation instead of default portrait * @returns the pdf document definition ready to export */ - private async getStandardPaper(documentContent: object, metadata?: object, imageUrls?: string[]): Promise { + private async getStandardPaper( + documentContent: object, + metadata?: object, + imageUrls?: string[], + customMargins?: [number, number, number, number], + landscape?: boolean + ): Promise { this.initFonts(); this.imageUrls = imageUrls ? imageUrls : []; pdfMake.vfs = await this.initVfs(); const result = { pageSize: 'A4', - pageMargins: [75, 90, 75, 75], + pageOrientation: landscape ? landscape : 'portrait', + pageMargins: customMargins || [75, 90, 75, 75], defaultStyle: { font: 'PdfFont', fontSize: this.configService.instant('general_export_pdf_fontsize') }, - header: this.getHeader(), + header: this.getHeader(customMargins ? [customMargins[0], customMargins[2]] : null), // TODO: option for no footer, wherever this can be defined footer: (currentPage, pageCount) => { - return this.getFooter(currentPage, pageCount); + return this.getFooter( + currentPage, + pageCount, + customMargins ? [customMargins[0], customMargins[2]] : null + ); }, info: metadata, content: documentContent, @@ -188,9 +201,10 @@ export class PdfDocumentService { /** * Creates the header doc definition for normal PDF documents * + * @param lrMargin optional margin overrides * @returns an object that contains the necessary header definition */ - private getHeader(): object { + private getHeader(lrMargin?: [number, number]): object { // check for the required logos let logoHeaderLeftUrl = this.configService.instant('logo_pdf_header_L').path; let logoHeaderRightUrl = this.configService.instant('logo_pdf_header_R').path; @@ -247,11 +261,13 @@ export class PdfDocumentService { }); this.imageUrls.push(logoHeaderRightUrl); } + const margin = [lrMargin ? lrMargin[0] : 75, 30, lrMargin ? lrMargin[0] : 75, 10]; + // pdfmake order: [left, top, right, bottom] return { color: '#555', fontSize: 9, - margin: [75, 30, 75, 10], // [left, top, right, bottom] + margin: margin, columns: columns, columnGap: 10 }; @@ -265,9 +281,10 @@ export class PdfDocumentService { * * @param currentPage holds the number of the current page * @param pageCount holds the page count + * @param lrMargin optionally overriding the margins * @returns the footer doc definition */ - private getFooter(currentPage: number, pageCount: number): object { + private getFooter(currentPage: number, pageCount: number, lrMargin?: [number, number]): object { const columns = []; let logoContainerWidth: string; let pageNumberPosition: string; @@ -330,8 +347,9 @@ export class PdfDocumentService { this.imageUrls.push(logoFooterRightUrl); } + const margin = [lrMargin ? lrMargin[0] : 75, 0, lrMargin ? lrMargin[0] : 75, 10]; return { - margin: [75, 0, 75, 10], + margin: margin, columns: columns, columnGap: 10 }; @@ -359,6 +377,18 @@ export class PdfDocumentService { }); } + /** + * Downloads a pdf in landscape orientation + * + * @param docDefinition the structure of the PDF document + * @param filename the name of the file to use + * @param metadata + */ + public downloadLandscape(docDefinition: object, filename: string, metadata?: object): void { + this.getStandardPaper(docDefinition, metadata, null, [50, 80, 50, 75], true).then(doc => { + this.createPdf(doc, filename); + }); + } /** * Downloads a pdf with the ballot papet page definitions. * diff --git a/client/src/app/site/motions/components/call-list/call-list.component.html b/client/src/app/site/motions/components/call-list/call-list.component.html index 671602a43..cf0f13e20 100644 --- a/client/src/app/site/motions/components/call-list/call-list.component.html +++ b/client/src/app/site/motions/components/call-list/call-list.component.html @@ -4,10 +4,10 @@

Call list

- + @@ -15,7 +15,25 @@ - + + + + + + + diff --git a/client/src/app/site/motions/components/call-list/call-list.component.ts b/client/src/app/site/motions/components/call-list/call-list.component.ts index 33fce5510..f003e6201 100644 --- a/client/src/app/site/motions/components/call-list/call-list.component.ts +++ b/client/src/app/site/motions/components/call-list/call-list.component.ts @@ -7,9 +7,11 @@ import { Observable } from 'rxjs'; import { BaseViewComponent } from '../../../base/base-view'; import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service'; +import { MotionCsvExportService } from '../../services/motion-csv-export.service'; +import { MotionPdfExportService } from '../../services/motion-pdf-export.service'; + import { ViewMotion } from '../../models/view-motion'; import { OSTreeSortEvent } from 'app/shared/components/sorting-tree/sorting-tree.component'; -import { MotionCsvExportService } from '../../services/motion-csv-export.service'; /** * Sort view for the call list. @@ -46,7 +48,8 @@ export class CallListComponent extends BaseViewComponent { translate: TranslateService, matSnackBar: MatSnackBar, private motionRepo: MotionRepositoryService, - private motionCsvExport: MotionCsvExportService + private motionCsvExport: MotionCsvExportService, + private motionPdfExport: MotionPdfExportService ) { super(title, translate, matSnackBar); @@ -83,4 +86,11 @@ export class CallListComponent extends BaseViewComponent { public csvExportCallList(): void { this.motionCsvExport.exportCallList(this.motions); } + + /** + * Triggers a pdf export of the call list + */ + public pdfExportCallList(): void { + this.motionPdfExport.exportPdfCallList(this.motions); + } } diff --git a/client/src/app/site/motions/services/motion-pdf-export.service.ts b/client/src/app/site/motions/services/motion-pdf-export.service.ts index 06853cafe..8816913f2 100644 --- a/client/src/app/site/motions/services/motion-pdf-export.service.ts +++ b/client/src/app/site/motions/services/motion-pdf-export.service.ts @@ -70,4 +70,18 @@ export class MotionPdfExportService { }; this.pdfDocumentService.download(doc, filename, metadata); } + + /** + * Exports a table of the motions in order of their call list + * + * @param motions the motions to export + */ + public exportPdfCallList(motions: ViewMotion[]): void { + const doc = this.motionPdfService.callListToDoc(motions); + const filename = this.translate.instant('Call list'); + const metadata = { + title: filename + }; + this.pdfDocumentService.downloadLandscape(doc, filename, metadata); + } } diff --git a/client/src/app/site/motions/services/motion-pdf.service.ts b/client/src/app/site/motions/services/motion-pdf.service.ts index 87397abd4..c115d9f9a 100644 --- a/client/src/app/site/motions/services/motion-pdf.service.ts +++ b/client/src/app/site/motions/services/motion-pdf.service.ts @@ -2,12 +2,12 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { ViewMotion, LineNumberingMode, ChangeRecoMode } from '../models/view-motion'; -import { MotionRepositoryService } from '../../../core/repositories/motions/motion-repository.service'; -import { ConfigService } from 'app/core/ui-services/config.service'; import { ChangeRecommendationRepositoryService } from '../../../core/repositories/motions/change-recommendation-repository.service'; -import { ViewUnifiedChange } from '../models/view-unified-change'; +import { ConfigService } from 'app/core/ui-services/config.service'; +import { MotionRepositoryService } from '../../../core/repositories/motions/motion-repository.service'; import { HtmlToPdfService } from 'app/core/ui-services/html-to-pdf.service'; +import { ViewMotion, LineNumberingMode, ChangeRecoMode } from '../models/view-motion'; +import { ViewUnifiedChange } from '../models/view-unified-change'; /** * Converts a motion to pdf. Can be used from the motion detail view or executed on a list of motions @@ -413,4 +413,88 @@ export class MotionPdfService { return {}; } } + + /** + * Creates pdfMake definitions for the call list of given motions + * + * @param motions A list of motions + * @returns definitions ready to be opened or exported via {@link PdfDocumentService} + */ + public callListToDoc(motions: ViewMotion[]): object { + motions.sort((a, b) => a.callListWeight - b.callListWeight); + 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' + }, + { + text: this.translate.instant('Motion block'), + style: 'tableHeader' + } + ] + ]; + + const callListRows = motions.map(motion => this.createCallListRow(motion)); + const table: object = { + table: { + widths: ['auto', 'auto', 'auto', '*', 'auto', 'auto'], + headerRows: 1, + body: callListTableBody.concat(callListRows) + }, + layout: { + hLineWidth: rowIndex => { + return rowIndex === 1; + }, + vLineWidth: () => { + return 0; + }, + fillColor: rowIndex => { + return rowIndex % 2 === 0 ? '#EEEEEE' : null; + } + } + }; + 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 : '' }, + { text: motion.submitters.length ? motion.submitters.map(s => s.short_name).join(', ') : '' }, + { text: motion.title }, + { + text: motion.recommendation ? this.translate.instant(motion.recommendation.recommendation_label) : '' + }, + { text: motion.motion_block ? motion.motion_block.title : '' } + ]; + } }