diff --git a/client/src/app/site/pages/meetings/pages/agenda/pages/agenda-export/components/agenda-export/agenda-export.component.html b/client/src/app/site/pages/meetings/pages/agenda/pages/agenda-export/components/agenda-export/agenda-export.component.html index f6cd7bab010..5192af4fd1d 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/pages/agenda-export/components/agenda-export/agenda-export.component.html +++ b/client/src/app/site/pages/meetings/pages/agenda/pages/agenda-export/components/agenda-export/agenda-export.component.html @@ -17,6 +17,9 @@

Agenda export

+ + + @@ -170,7 +173,7 @@

Agenda export

} - @if (isCSVFormat) { + @if (isCSVFormat || isXMLFormat) {

{{ 'Meta information' | translate }}

diff --git a/client/src/app/site/pages/meetings/pages/agenda/pages/agenda-export/components/agenda-export/agenda-export.component.ts b/client/src/app/site/pages/meetings/pages/agenda/pages/agenda-export/components/agenda-export/agenda-export.component.ts index c6bfbe97e05..fcdd9fb86f4 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/pages/agenda-export/components/agenda-export/agenda-export.component.ts +++ b/client/src/app/site/pages/meetings/pages/agenda/pages/agenda-export/components/agenda-export/agenda-export.component.ts @@ -99,6 +99,10 @@ export class AgendaExportComponent extends BaseComponent implements OnDestroy, A return this.fileFormats[this.tabIndex] === ExportFileFormat.CSV; } + public get isXMLFormat(): boolean { + return this.fileFormats[this.tabIndex] === ExportFileFormat.XML; + } + public agendaItems: Id[] = []; private pdfDefaults = { @@ -112,12 +116,14 @@ export class AgendaExportComponent extends BaseComponent implements OnDestroy, A metaInfo: [`duration`] }; + private xmlDefaults = this.csvDefaults; + private tabIndex = 0; // Store fileformats with corresponding tab group index - private fileFormats: ExportFileFormat[] = [ExportFileFormat.PDF, ExportFileFormat.CSV]; + private fileFormats: ExportFileFormat[] = [ExportFileFormat.PDF, ExportFileFormat.CSV, ExportFileFormat.XML]; private savedSelections: SavedSelections = { tab_index: 0, - tab_selections: [this.pdfDefaults, this.csvDefaults] + tab_selections: [this.pdfDefaults, this.csvDefaults, this.xmlDefaults] }; private backNr: string; @@ -176,6 +182,9 @@ export class AgendaExportComponent extends BaseComponent implements OnDestroy, A } else if (this.isCSVFormat) { const csvMetaInfo = this.dialogForm.get(`metaInfo`).value ?? []; this.agendaExportService.exportAsCsv(views, info, csvMetaInfo); + } else if (this.isXMLFormat) { + const metaInfo = this.dialogForm.get(`metaInfo`).value ?? []; + this.agendaExportService.exportAsXML(views, info, metaInfo); } this.router.navigate([this.activeMeetingIdService.meetingId, `agenda`]); } diff --git a/client/src/app/site/pages/meetings/pages/agenda/pages/agenda-item-list/services/agenda-item-export.service/agenda-item-export.service.ts b/client/src/app/site/pages/meetings/pages/agenda/pages/agenda-item-list/services/agenda-item-export.service/agenda-item-export.service.ts index 37f8f87f340..675f0ab683c 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/pages/agenda-item-list/services/agenda-item-export.service/agenda-item-export.service.ts +++ b/client/src/app/site/pages/meetings/pages/agenda/pages/agenda-item-list/services/agenda-item-export.service/agenda-item-export.service.ts @@ -2,13 +2,15 @@ import { inject, Service } from '@angular/core'; import { ViewAgendaItem } from '@app/site/pages/meetings/pages/agenda'; import { MeetingPdfExportService } from '@app/site/pages/meetings/services/export'; import { MeetingCsvExportForBackendService } from '@app/site/pages/meetings/services/export/meeting-csv-export-for-backend.service'; +import { MeetingXmlExportService } from '@app/site/pages/meetings/services/export/meeting-xml-export.service'; import { TranslateService } from '@ngx-translate/core'; import { AgendaPdfCatalogExportService } from '../../../../services/agenda-pdf-catalog-export.service/agenda-pdf-catalog-export.service'; export enum ExportFileFormat { PDF = 0, - CSV + CSV = 1, + XML = 2 } export type InfoToExport = @@ -25,12 +27,15 @@ export type pdfMetaInfo = `table_of_content` | `line_break` | `header` | `footer export type csvMetaInfo = `duration` | `tags` | `agenda_visibility` | `done`; +export type xmlMetaInfo = csvMetaInfo; + @Service() export class AgendaItemExportService { private translate = inject(TranslateService); private csvExportService = inject(MeetingCsvExportForBackendService); private pdfExportService = inject(MeetingPdfExportService); private agendaPdfExportService = inject(AgendaPdfCatalogExportService); + private xmlExportService = inject(MeetingXmlExportService); public exportAsCsv(source: ViewAgendaItem[], info: InfoToExport[], csvMeta: csvMetaInfo): void { const config = []; @@ -98,4 +103,10 @@ export class AgendaItemExportService { filename }); } + + public exportAsXML(source: ViewAgendaItem[], info: InfoToExport[], xmlMeta: xmlMetaInfo[]): void { + const filename = this.translate.instant(`Agenda`) + `.xml`; + const config: (InfoToExport | csvMetaInfo)[] = [...info, ...xmlMeta]; + this.xmlExportService.export(source, filename, config); + } } diff --git a/client/src/app/site/pages/meetings/services/export/meeting-xml-export.service.ts b/client/src/app/site/pages/meetings/services/export/meeting-xml-export.service.ts new file mode 100644 index 00000000000..857a6a04a65 --- /dev/null +++ b/client/src/app/site/pages/meetings/services/export/meeting-xml-export.service.ts @@ -0,0 +1,86 @@ +import { inject, Service } from '@angular/core'; +import { FileExportService } from '@app/gateways/export/file-export.service'; + +import { ViewAgendaItem } from '../../pages/agenda'; +import { + csvMetaInfo, + InfoToExport, + xmlMetaInfo +} from '../../pages/agenda/pages/agenda-item-list/services/agenda-item-export.service/agenda-item-export.service'; + +@Service() +export class MeetingXmlExportService { + private exporter = inject(FileExportService); + private serializer = new XMLSerializer(); + private itemMap; + + public export(source: ViewAgendaItem[], filename: string, config: (InfoToExport | csvMetaInfo)[]): void { + const xml = this.generateXML(source, config); + this.exporter.saveFile(xml, filename, 'application/xml'); + } + + private generateXML(source: ViewAgendaItem[], config: (InfoToExport | csvMetaInfo)[]): string { + const doc = document.implementation.createDocument('', 'Agenda-Export', null); + const root = doc.documentElement; + const mainAgendaTopics = source.filter(item => !item.parent_id); + this.itemMap = new Map(source.map(item => [item.id, item])); + for (const item of mainAgendaTopics) { + root.appendChild(this.createAgendaTopic(item, doc, config)); + } + return this.serializer.serializeToString(doc); + } + + private createAgendaTopic( + item: ViewAgendaItem, + doc: XMLDocument, + config: (InfoToExport | csvMetaInfo)[], + isNested = false + ): HTMLElement { + const agendaItem = doc.createElement(isNested ? 'Sub-Agenda-Item' : 'Agenda-Item'); + + const addContentNode = ( + config: (InfoToExport | csvMetaInfo)[], + name: InfoToExport | xmlMetaInfo, + value: unknown + ): void => { + if ( + (config.includes(name as InfoToExport) || config.includes(name as xmlMetaInfo)) && + value !== undefined && + value !== null && + value !== '' && + value !== 0 + ) { + const node = doc.createElement(name.replaceAll('_', '-')); + node.textContent = String(value); + agendaItem.appendChild(node); + } + }; + + addContentNode(config, 'item_number', item.item_number); + addContentNode(config, 'title', item.content_object?.title); + addContentNode(config, 'text', item.content_object?.text); + addContentNode(config, 'moderation_notes', item.content_object?.list_of_speakers?.moderator_notes); + addContentNode(config, 'list_of_speakers', item.content_object?.list_of_speakers?.speaker_ids?.length); + addContentNode(config, 'internal_commentary', item.comment); + addContentNode(config, 'duration', item.duration); + addContentNode(config, 'agenda_visibility', item.type); + addContentNode(config, 'done', item.closed); + + if (item.tags?.length && config.includes('tags')) { + const tags = doc.createElement('tags'); + item.tags.filter(tagName => { + const tag = doc.createElement('tag'); + tag.textContent = tagName.tag.name; + tags.appendChild(tag); + }); + agendaItem.appendChild(tags); + } + item.child_ids?.forEach(childId => { + const child = this.createAgendaTopic(this.itemMap.get(childId), doc, config, true); + if (child) { + agendaItem.appendChild(child); + } + }); + return agendaItem; + } +}