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;
+ }
+}