Skip to content

Commit 8086f6d

Browse files
committed
feat: add CSV import functionality for confirmed participants
1 parent d369e83 commit 8086f6d

5 files changed

Lines changed: 177 additions & 0 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"features": {
3+
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {
4+
"version": "1.10.0",
5+
"resolved": "ghcr.io/devcontainers/features/docker-outside-of-docker@sha256:c2c2cf829505ead8e4892c88c31b6594ae94a2bbb209e16e1fac456c1a3a624e",
6+
"integrity": "sha256:c2c2cf829505ead8e4892c88c31b6594ae94a2bbb209e16e1fac456c1a3a624e"
7+
},
8+
"ghcr.io/devcontainers/features/github-cli:1": {
9+
"version": "1.1.0",
10+
"resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671",
11+
"integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671"
12+
}
13+
}
14+
}

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"editor.codeActionsOnSave": {
2525
"source.fixAll.eslint": "explicit"
2626
},
27+
"eslint.workingDirectories": [{ "mode": "auto" }],
2728
"editor.tabSize": 2,
2829
"files.associations": {
2930
"*.mjml": "html"

apps/api/src/routes/exports/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { makeApp } from '../../util/make-app.js'
22
import { authorize } from './middleware/authorize.js'
33
import { veranstaltungPhotoArchive } from './photos.archive.js'
4+
import { veranstaltungRechnungsimport } from './rechnungsimport.csv.js'
45
import { veranstaltungTeilnehmendenliste } from './teilnehmendenliste.sheet.js'
56
import { veranstaltungUnterschriftenliste } from './unterschriftenliste.sheet.js'
67
import { veranstaltungVerpflegung } from './verpflegung.sheet.js'
@@ -11,5 +12,6 @@ const exportRouter = makeApp()
1112
.get('/sheet/unterschriftenliste', veranstaltungUnterschriftenliste)
1213
.get('/sheet/verpflegung', veranstaltungVerpflegung)
1314
.get('/archive/photos', veranstaltungPhotoArchive)
15+
.get('/csv/rechnungsimport', veranstaltungRechnungsimport)
1416

1517
export { exportRouter }
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import dayjs from 'dayjs'
2+
import type { Context } from 'hono'
3+
import prisma from '../../prisma.js'
4+
import type { AuthorizeResults } from './middleware/authorize.js'
5+
6+
const COLUMNS = [
7+
'Mitgliedsnummer',
8+
'Datensatztyp',
9+
'Rechnungsinkrement',
10+
'Rechnungsbezeichnung',
11+
'Datum',
12+
'Positionsinkrement',
13+
'Positionsbezeichnung',
14+
'Positionsbeschreibung',
15+
'Menge',
16+
'Einzelpreis(Brutto)',
17+
'Inkasso',
18+
'Zustellung',
19+
'Zahlungsziel',
20+
'Intervall',
21+
'Termin',
22+
'Fälligkeit',
23+
'Ende',
24+
'Mwst',
25+
'Rechnungsvermerk',
26+
'Spendenfähig',
27+
'Spendenart',
28+
'Buchhaltungskonto',
29+
'Steuerschlüssel',
30+
'Kostenstelle',
31+
'Auswertungskennziffer',
32+
'Nachlass',
33+
'Nachlassgrund',
34+
'Empfänger-Email',
35+
'Zusatzinformationen',
36+
]
37+
38+
function escapeCsvField(value: string): string {
39+
if (value.includes(';') || value.includes('"') || value.includes('\n')) {
40+
return `"${value.replace(/"/g, '""')}"`
41+
}
42+
return value
43+
}
44+
45+
export async function veranstaltungRechnungsimport(ctx: Context<{ Variables: AuthorizeResults }>) {
46+
const { query, gliederung } = ctx.var
47+
48+
if (!query.veranstaltungId) {
49+
return ctx.text('veranstaltungId is required', 400)
50+
}
51+
52+
const veranstaltung = await prisma.veranstaltung.findUnique({
53+
where: { id: query.veranstaltungId },
54+
select: {
55+
name: true,
56+
teilnahmegebuehr: true,
57+
unterveranstaltungen: {
58+
where: { gliederungId: gliederung?.id },
59+
select: {
60+
Anmeldung: {
61+
where: { status: 'BESTAETIGT' },
62+
select: {
63+
person: {
64+
select: {
65+
email: true,
66+
},
67+
},
68+
customFieldValues: {
69+
select: {
70+
value: true,
71+
field: {
72+
select: {
73+
name: true,
74+
},
75+
},
76+
},
77+
},
78+
},
79+
},
80+
},
81+
},
82+
},
83+
})
84+
85+
if (!veranstaltung) {
86+
return ctx.text('Veranstaltung not found', 404)
87+
}
88+
89+
const anmeldungenList = veranstaltung.unterveranstaltungen
90+
.flatMap((unterveranstaltung) => unterveranstaltung.Anmeldung)
91+
.sort((a, b) => a.person.email.localeCompare(b.person.email))
92+
93+
const today = dayjs().format('DD.MM.YYYY')
94+
const faelligkeit = dayjs().add(14, 'day').format('DD.MM.YYYY')
95+
96+
const rechnungsbezeichnung = veranstaltung.name.substring(0, 70)
97+
const positionsbezeichnung = `Teilnahmegebühr ${veranstaltung.name}`.substring(0, 70)
98+
const preis = veranstaltung.teilnahmegebuehr.toFixed(2).replace('.', ',')
99+
100+
const rows = anmeldungenList.map((anmeldung) => {
101+
const mitgliedsnummerValue = anmeldung.customFieldValues.find(
102+
(cfv) => cfv.field.name.toLowerCase() === 'mitgliedsnummer'
103+
)?.value
104+
const mitgliedsnummer =
105+
typeof mitgliedsnummerValue === 'string'
106+
? mitgliedsnummerValue
107+
: typeof mitgliedsnummerValue === 'number'
108+
? String(mitgliedsnummerValue)
109+
: ''
110+
111+
return [
112+
mitgliedsnummer,
113+
'2',
114+
'1',
115+
rechnungsbezeichnung,
116+
today,
117+
'1',
118+
positionsbezeichnung,
119+
'',
120+
'1',
121+
preis,
122+
'2',
123+
'2',
124+
'',
125+
'0',
126+
today,
127+
faelligkeit,
128+
'31.12.2099',
129+
'0',
130+
'',
131+
'0',
132+
'',
133+
'',
134+
'',
135+
'',
136+
'',
137+
'',
138+
'',
139+
anmeldung.person.email,
140+
'',
141+
]
142+
})
143+
144+
const csvLines = [COLUMNS.join(';'), ...rows.map((row) => row.map(escapeCsvField).join(';'))]
145+
146+
const csvContent = '' + csvLines.join('\r\n')
147+
const filename = `${dayjs().format('YYYYMMDD-HHmm')}-Rechnungsimport.csv`
148+
149+
ctx.header('Content-Disposition', `attachment; filename="${filename}"`)
150+
ctx.header('Content-Type', 'text/csv; charset=utf-8')
151+
return ctx.body(csvContent, 200)
152+
}

apps/frontend/src/views/Verwaltung/Veranstaltungen/VeranstaltungDetail.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,14 @@ const files: ExportedFileType[] = [
135135
hoverColor: 'hover:text-orange-700',
136136
href: `/api/export/archive/photos?${exportParams}&mode=flat`,
137137
},
138+
{
139+
name: 'Rechnungsimport',
140+
initial: 'RE',
141+
href: `/api/export/csv/rechnungsimport?${exportParams}`,
142+
description: 'CSV-Import für bestätigte Teilnehmende (Fakturierung)',
143+
bgColor: 'bg-indigo-600',
144+
hoverColor: 'hover:text-indigo-700',
145+
},
138146
]
139147
140148
const publicProgramLink = computed(() => {

0 commit comments

Comments
 (0)