From 160176532f19d0e7d1cf49b5c762ef33e6dffcf2 Mon Sep 17 00:00:00 2001 From: Javier Borrego Date: Fri, 31 Jul 2026 10:49:54 +0200 Subject: [PATCH 1/3] XML agenda export --- .../agenda-export.component.html | 8 ++ .../agenda-export/agenda-export.component.ts | 21 +++- .../agenda-item-export.service.ts | 53 +++++++++- .../export/meeting-xml-export.service.ts | 100 ++++++++++++++++++ 4 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 client/src/app/site/pages/meetings/services/export/meeting-xml-export.service.ts 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..c02823a2bdf 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

+ + + @@ -216,3 +219,8 @@

Agenda export

+ +
+
+
+
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..5f045f172c7 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,17 @@ export class AgendaExportComponent extends BaseComponent implements OnDestroy, A metaInfo: [`duration`] }; + private xmlDefaults = { + content: ['item_nuber', 'title', 'text', 'attachments', 'moderation_notes'], + metaInfo: ['duration'] + }; + 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; @@ -173,11 +182,15 @@ export class AgendaExportComponent extends BaseComponent implements OnDestroy, A ...(this.dialogForm.get(`headerFooter`).value ?? []) ]; this.agendaExportService.exportAsPdf(views, info, pdfMeta); - } else if (this.isCSVFormat) { + } + if (this.isCSVFormat) { const csvMetaInfo = this.dialogForm.get(`metaInfo`).value ?? []; this.agendaExportService.exportAsCsv(views, info, csvMetaInfo); + } else if (this.isXMLFormat) { + const xmlMeta = []; + this.agendaExportService.exportAsXML(views, info, xmlMeta); } - this.router.navigate([this.activeMeetingIdService.meetingId, `agenda`]); + // this.router.navigate([this.activeMeetingIdService.meetingId, `agenda`]); } public afterTabChanged(): void { 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..b4d7f967d35 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 = `test_info`; + @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 = []; @@ -93,9 +98,53 @@ export class AgendaItemExportService { const filename = this.translate.instant(`Agenda`); const metaExportInfo = { pdfOptions: [...meta] }; this.pdfExportService.download({ - docDefinition: this.agendaPdfExportService.agendaListToDocDef(source, info, meta), + docDefinition: this.agendaPdfExportService.agendaListToDocDef([...source], info, meta), exportInfo: metaExportInfo, filename }); } + + public exportAsXML(source: ViewAgendaItem[], info: InfoToExport[], meta: xmlMetaInfo[]): void { + const config = []; + if (info.includes(`item_number`)) { + config.push({ label: `item_number`, property: `item_number` }); + } + if (info.includes(`title`)) { + config.push({ label: `title`, map: (viewItem): string => viewItem.content_object?.title }); + } + if (info.includes(`text`)) { + config.push({ + label: `text`, + map: (viewItem): string => + viewItem.content_object?.getCSVExportText ? viewItem.content_object.getCSVExportText() : `` + }); + } + if (info.includes(`moderation_notes`)) { + config.push({ + label: `moderation_notes`, + map: (viewItem): string => viewItem.content_object?.list_of_speakers?.moderator_notes ?? `` + }); + } + if (info.includes(`list_of_speakers`)) { + config.push({ + label: `list_of_speakers`, + map: (viewItem): string => { + if ( + viewItem.content_object?.list_of_speakers && + viewItem.content_object.list_of_speakers.waitingSpeakerAmount > 0 + ) { + return viewItem.content_object?.list_of_speakers.waitingSpeakerAmount.toString(); + } + return ``; + } + }); + } + if (info.includes(`internal_commentary`)) { + config.push({ label: `agenda_comment`, property: `comment` }); + } + + const filename = this.translate.instant(`Agenda`) + `.xml`; + this.xmlExportService.export(source, config, filename); + console.log('Exported as XML', source, info, filename, meta); + } } 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..8bd930ba3cb --- /dev/null +++ b/client/src/app/site/pages/meetings/services/export/meeting-xml-export.service.ts @@ -0,0 +1,100 @@ +import { inject, Service } from '@angular/core'; +import { FileExportService } from '@app/gateways/export/file-export.service'; + +import { ViewAgendaItem } from '../../pages/agenda'; +import { ViewAssignment } from '../../pages/assignments'; +import { ViewMotion, ViewMotionBlock } from '../../pages/motions'; +import { InfoToExport } from '../../pages/motions/services/export/definitions'; +import { MeetingSettingsService } from '../meeting-settings.service'; + +@Service() +export class MeetingXmlExportService { + private meetingSettings = inject(MeetingSettingsService); + private exporter = inject(FileExportService); + private serializer = new XMLSerializer(); + + public export(source: ViewAgendaItem[], info: InfoToExport[], filename: string): void { + const xml = this.generateXML(source); + this.exporter.saveFile(xml, filename, 'application/xml'); + } + + private generateXML(source: ViewAgendaItem[]): string { + const doc = document.implementation.createDocument('', 'Agenda-Export', null); + const root = doc.documentElement; + + source.forEach(item => { + const agendaItem = doc.createElement('Agenda-Item'); + if (item.content_object.collection === 'topic') { + if (item.content_object?.item_number) { + const item_number = doc.createElement('item-number'); + item_number.textContent = item.content_object?.item_number; + agendaItem.appendChild(item_number); + } + if (item.content_object?.title) { + const title = doc.createElement('title'); + title.textContent = item.content_object.title; + agendaItem.appendChild(title); + } + if (item.content_object?.text) { + const text = doc.createElement('text'); + text.textContent = item.content_object.text; + agendaItem.appendChild(text); + } + if (item.content_object?.list_of_speakers.moderator_notes) { + const moderator_notes = doc.createElement('moderation_notes'); + moderator_notes.textContent = item.content_object?.list_of_speakers.moderator_notes; + agendaItem.appendChild(moderator_notes); + } + if (item.content_object?.list_of_speakers?.speaker_ids) { + const list_of_speakers = doc.createElement('list_of_speakers'); + list_of_speakers.textContent = String(item.content_object.list_of_speakers?.speaker_ids?.length); + agendaItem.appendChild(list_of_speakers); + } + if (item.comment) { + const agenda_comment = doc.createElement('agenda_comment'); + agenda_comment.textContent = String(item.comment); + agendaItem.appendChild(agenda_comment); + } + if (item.duration) { + const agenda_duration = doc.createElement('agenda_duration'); + agenda_duration.textContent = String(item.duration); + agendaItem.appendChild(agenda_duration); + } + if (item.type) { + const agenda_type = doc.createElement('agenda_type'); + agenda_type.textContent = String(item.type); + agendaItem.appendChild(agenda_type); + } + if (item.tags?.length) { + const tags = doc.createElement('tags'); + item.tags.filter(t => { + const tag = doc.createElement('tag'); + tag.textContent = t.tag.name; + tags.appendChild(tag); + }); + agendaItem.appendChild(tags); + } + if (item.closed) { + const agenda_closed = doc.createElement('agenda_closed'); + agenda_closed.textContent = String(item.closed); + agendaItem.appendChild(agenda_closed); + } + } + if (item.content_object.collection === 'assignment') { + const assignment: ViewAssignment = item.content_object; + console.log('assignment', assignment); + } + if (item.content_object.collection === 'motion') { + const motion: ViewMotion = item.content_object; + console.log('motion', motion); + } + if (item.content_object.collection === 'motion_block') { + const motion_block: ViewMotionBlock = item.content_object; + console.log('motion_block', motion_block); + } + + root.appendChild(agendaItem); + }); + return this.serializer.serializeToString(doc); + } +} From 9275222e3562042aa199744f65594692927b17fb Mon Sep 17 00:00:00 2001 From: Javier Borrego Date: Fri, 31 Jul 2026 12:56:40 +0200 Subject: [PATCH 2/3] Added nested agenda items support --- .../agenda-export/agenda-export.component.ts | 2 +- .../agenda-item-export.service.ts | 43 +---- .../export/meeting-xml-export.service.ts | 154 +++++++++--------- 3 files changed, 80 insertions(+), 119 deletions(-) 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 5f045f172c7..c309f725708 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 @@ -188,7 +188,7 @@ export class AgendaExportComponent extends BaseComponent implements OnDestroy, A this.agendaExportService.exportAsCsv(views, info, csvMetaInfo); } else if (this.isXMLFormat) { const xmlMeta = []; - this.agendaExportService.exportAsXML(views, info, xmlMeta); + this.agendaExportService.exportAsXML(views); } // 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 b4d7f967d35..ef6e34c4261 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 @@ -104,47 +104,8 @@ export class AgendaItemExportService { }); } - public exportAsXML(source: ViewAgendaItem[], info: InfoToExport[], meta: xmlMetaInfo[]): void { - const config = []; - if (info.includes(`item_number`)) { - config.push({ label: `item_number`, property: `item_number` }); - } - if (info.includes(`title`)) { - config.push({ label: `title`, map: (viewItem): string => viewItem.content_object?.title }); - } - if (info.includes(`text`)) { - config.push({ - label: `text`, - map: (viewItem): string => - viewItem.content_object?.getCSVExportText ? viewItem.content_object.getCSVExportText() : `` - }); - } - if (info.includes(`moderation_notes`)) { - config.push({ - label: `moderation_notes`, - map: (viewItem): string => viewItem.content_object?.list_of_speakers?.moderator_notes ?? `` - }); - } - if (info.includes(`list_of_speakers`)) { - config.push({ - label: `list_of_speakers`, - map: (viewItem): string => { - if ( - viewItem.content_object?.list_of_speakers && - viewItem.content_object.list_of_speakers.waitingSpeakerAmount > 0 - ) { - return viewItem.content_object?.list_of_speakers.waitingSpeakerAmount.toString(); - } - return ``; - } - }); - } - if (info.includes(`internal_commentary`)) { - config.push({ label: `agenda_comment`, property: `comment` }); - } - + public exportAsXML(source: ViewAgendaItem[]): void { const filename = this.translate.instant(`Agenda`) + `.xml`; - this.xmlExportService.export(source, config, filename); - console.log('Exported as XML', source, info, filename, meta); + this.xmlExportService.export(source, filename); } } 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 index 8bd930ba3cb..f96cbf96ce2 100644 --- 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 @@ -2,18 +2,14 @@ import { inject, Service } from '@angular/core'; import { FileExportService } from '@app/gateways/export/file-export.service'; import { ViewAgendaItem } from '../../pages/agenda'; -import { ViewAssignment } from '../../pages/assignments'; -import { ViewMotion, ViewMotionBlock } from '../../pages/motions'; import { InfoToExport } from '../../pages/motions/services/export/definitions'; -import { MeetingSettingsService } from '../meeting-settings.service'; @Service() export class MeetingXmlExportService { - private meetingSettings = inject(MeetingSettingsService); private exporter = inject(FileExportService); private serializer = new XMLSerializer(); - public export(source: ViewAgendaItem[], info: InfoToExport[], filename: string): void { + public export(source: ViewAgendaItem[], filename: string): void { const xml = this.generateXML(source); this.exporter.saveFile(xml, filename, 'application/xml'); } @@ -21,80 +17,84 @@ export class MeetingXmlExportService { private generateXML(source: ViewAgendaItem[]): string { const doc = document.implementation.createDocument('', 'Agenda-Export', null); const root = doc.documentElement; + const itemMap = new Map(source.map(item => [item.id, item])); + const mainAgendaTopics = source.filter(item => !item.parent_id); + for (const item of mainAgendaTopics) { + root.appendChild(this.createAgendaTopic(item, doc, itemMap, false)); + } + return this.serializer.serializeToString(doc); + } - source.forEach(item => { - const agendaItem = doc.createElement('Agenda-Item'); - if (item.content_object.collection === 'topic') { - if (item.content_object?.item_number) { - const item_number = doc.createElement('item-number'); - item_number.textContent = item.content_object?.item_number; - agendaItem.appendChild(item_number); - } - if (item.content_object?.title) { - const title = doc.createElement('title'); - title.textContent = item.content_object.title; - agendaItem.appendChild(title); - } - if (item.content_object?.text) { - const text = doc.createElement('text'); - text.textContent = item.content_object.text; - agendaItem.appendChild(text); - } - if (item.content_object?.list_of_speakers.moderator_notes) { - const moderator_notes = doc.createElement('moderation_notes'); - moderator_notes.textContent = item.content_object?.list_of_speakers.moderator_notes; - agendaItem.appendChild(moderator_notes); - } - if (item.content_object?.list_of_speakers?.speaker_ids) { - const list_of_speakers = doc.createElement('list_of_speakers'); - list_of_speakers.textContent = String(item.content_object.list_of_speakers?.speaker_ids?.length); - agendaItem.appendChild(list_of_speakers); - } - if (item.comment) { - const agenda_comment = doc.createElement('agenda_comment'); - agenda_comment.textContent = String(item.comment); - agendaItem.appendChild(agenda_comment); - } - if (item.duration) { - const agenda_duration = doc.createElement('agenda_duration'); - agenda_duration.textContent = String(item.duration); - agendaItem.appendChild(agenda_duration); - } - if (item.type) { - const agenda_type = doc.createElement('agenda_type'); - agenda_type.textContent = String(item.type); - agendaItem.appendChild(agenda_type); - } - if (item.tags?.length) { - const tags = doc.createElement('tags'); - item.tags.filter(t => { - const tag = doc.createElement('tag'); - tag.textContent = t.tag.name; - tags.appendChild(tag); - }); - agendaItem.appendChild(tags); - } - if (item.closed) { - const agenda_closed = doc.createElement('agenda_closed'); - agenda_closed.textContent = String(item.closed); - agendaItem.appendChild(agenda_closed); - } - } - if (item.content_object.collection === 'assignment') { - const assignment: ViewAssignment = item.content_object; - console.log('assignment', assignment); - } - if (item.content_object.collection === 'motion') { - const motion: ViewMotion = item.content_object; - console.log('motion', motion); - } - if (item.content_object.collection === 'motion_block') { - const motion_block: ViewMotionBlock = item.content_object; - console.log('motion_block', motion_block); + private createAgendaTopic( + item: ViewAgendaItem, + doc, + itemMap: Map>, + isNested = false + ): HTMLElement { + const agendaItem = doc.createElement(isNested ? 'Sub-Agenda-Item' : 'Agenda-Item'); + + if (item.item_number) { + const item_number = doc.createElement('item-number'); + item_number.textContent = item.item_number; + agendaItem.appendChild(item_number); + } + if (item.content_object?.title) { + const title = doc.createElement('title'); + title.textContent = item.content_object.title; + agendaItem.appendChild(title); + } + if (item.content_object?.text) { + const text = doc.createElement('text'); + text.textContent = item.content_object.text; + agendaItem.appendChild(text); + } + if (item.content_object?.list_of_speakers.moderator_notes) { + const moderator_notes = doc.createElement('moderation_notes'); + moderator_notes.textContent = item.content_object?.list_of_speakers.moderator_notes; + agendaItem.appendChild(moderator_notes); + } + if (item.content_object?.list_of_speakers?.speaker_ids) { + const list_of_speakers = doc.createElement('list_of_speakers'); + list_of_speakers.textContent = String(item.content_object.list_of_speakers?.speaker_ids?.length); + agendaItem.appendChild(list_of_speakers); + } + if (item.comment) { + const agenda_comment = doc.createElement('agenda_comment'); + agenda_comment.textContent = String(item.comment); + agendaItem.appendChild(agenda_comment); + } + if (item.duration) { + const agenda_duration = doc.createElement('agenda_duration'); + agenda_duration.textContent = String(item.duration); + agendaItem.appendChild(agenda_duration); + } + if (item.type) { + const agenda_type = doc.createElement('agenda_type'); + agenda_type.textContent = String(item.type); + agendaItem.appendChild(agenda_type); + } + if (item.tags?.length) { + const tags = doc.createElement('tags'); + item.tags.filter(t => { + const tag = doc.createElement('tag'); + tag.textContent = t.tag.name; + tags.appendChild(tag); + }); + agendaItem.appendChild(tags); + } + if (item.closed) { + const agenda_closed = doc.createElement('agenda_closed'); + agenda_closed.textContent = String(item.closed); + agendaItem.appendChild(agenda_closed); + } + + for (const childId of item.child_ids ?? []) { + const child = itemMap.get(childId); + if (child) { + agendaItem.appendChild(this.createAgendaTopic(child, doc, itemMap, true)); } + } - root.appendChild(agendaItem); - }); - return this.serializer.serializeToString(doc); + return agendaItem; } } From b5e7fd8a580cbf648c9d3f4bf03686e28cf89ef6 Mon Sep 17 00:00:00 2001 From: Javier Borrego Date: Fri, 31 Jul 2026 13:01:22 +0200 Subject: [PATCH 3/3] Cleanup --- .../components/agenda-export/agenda-export.component.ts | 1 - .../pages/meetings/services/export/meeting-xml-export.service.ts | 1 - 2 files changed, 2 deletions(-) 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 c309f725708..80ea280a9df 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 @@ -187,7 +187,6 @@ export class AgendaExportComponent extends BaseComponent implements OnDestroy, A const csvMetaInfo = this.dialogForm.get(`metaInfo`).value ?? []; this.agendaExportService.exportAsCsv(views, info, csvMetaInfo); } else if (this.isXMLFormat) { - const xmlMeta = []; this.agendaExportService.exportAsXML(views); } // this.router.navigate([this.activeMeetingIdService.meetingId, `agenda`]); 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 index f96cbf96ce2..56579150b8f 100644 --- 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 @@ -2,7 +2,6 @@ import { inject, Service } from '@angular/core'; import { FileExportService } from '@app/gateways/export/file-export.service'; import { ViewAgendaItem } from '../../pages/agenda'; -import { InfoToExport } from '../../pages/motions/services/export/definitions'; @Service() export class MeetingXmlExportService {