Skip to content
Merged

2.8.0 #414

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
42e97e5
feat: display own title in table
axelrindle Feb 9, 2026
9539778
feat: add additional fields to veranstaltungSelect for enhance d data…
danielswiatek Feb 10, 2026
b08b126
Merge branch 'development' of https://github.com/codeanker/brahmsee.d…
danielswiatek Feb 10, 2026
0a0c5a6
feat: add beschreibung and bedingungen fields to unterveranstaltungSe…
danielswiatek Feb 10, 2026
7d8e824
fix: remove unnecessary line and ensure isDeleting is accessed correc…
danielswiatek Feb 10, 2026
3fbccdb
Initial plan
Copilot Feb 10, 2026
549b377
Fix E2E test configuration and dependencies
Copilot Feb 10, 2026
3a9bfe4
Add comprehensive testing documentation
Copilot Feb 10, 2026
cf28d94
Remove redundant alias configuration from vitest config
Copilot Feb 10, 2026
ffd2480
Initial plan
Copilot Feb 10, 2026
8404548
Add signature list export functionality
Copilot Feb 10, 2026
97b490f
Fix HTTP status code for signature list export endpoint
Copilot Feb 10, 2026
a4ebe6d
Initial plan
Copilot Feb 10, 2026
7132465
Add JSDoc to packages/helpers utility functions
Copilot Feb 10, 2026
3d85dbb
Add JSDoc to apps/api/src/util backend utilities
Copilot Feb 10, 2026
5c79c0d
Add JSDoc to packages/validation utilities
Copilot Feb 10, 2026
752e1fb
Add JSDoc to apps/frontend/src/helpers utilities
Copilot Feb 10, 2026
df10198
Initial plan
Copilot Feb 10, 2026
d7830e0
Fix: Refresh event details data after saving in Marketing tab
Copilot Feb 10, 2026
4084588
Refactor: Extract mapLandingSettings to reduce duplication
Copilot Feb 10, 2026
a190faf
Merge pull request #412 from codeanker/copilot/add-jsdoc-documentation
danielswiatek Feb 11, 2026
63344be
Merge branch 'development' into copilot/fix-and-extend-tests
danielswiatek Feb 11, 2026
4fd547a
Merge remote-tracking branch 'origin/copilot/export-signature-list' i…
danielswiatek Feb 11, 2026
a9e66e9
Fix: Include street number in address for export and improve column w…
danielswiatek Feb 11, 2026
35f1378
Merge branch 'development' into copilot/fix-and-extend-tests
danielswiatek Feb 11, 2026
03d9fd9
Merge pull request #407 from codeanker/copilot/fix-and-extend-tests
danielswiatek Feb 11, 2026
6988da0
Fix: Update version to 2.8.0 in package.json
danielswiatek Feb 11, 2026
1dd11d2
Merge branch 'development' of https://github.com/codeanker/brahmsee.d…
danielswiatek Feb 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Testing

This document provides quick reference for running tests. For complete documentation, see `apps/docs/src/testing.md`.

## Quick Commands

### Unit Tests
```bash
# Run all unit tests
pnpm test

# Run in watch mode
pnpm test --watch

# Run with coverage
pnpm test --coverage
```

### E2E Tests

**Prerequisites** (one-time setup):
```bash
cd apps/frontend
pnpm exec playwright install
pnpm exec playwright install-deps
```

**Running E2E tests**:
```bash
# 1. Start services (in separate terminals)
pnpm run start:services # Docker services (database, etc.)
pnpm run start:api # Backend API
pnpm run start:frontend # Frontend dev server

# 2. Run tests
cd apps/frontend
NODE_ENV=test pnpm exec vitest --run -c ./vitest.config.e2e.ts
```

## Test Locations

- **Unit Tests**: `packages/*/src/*.test.ts`
- **E2E Tests**: `apps/frontend/tests/*.e2e.ts`
- **Test Utilities**: `apps/api/src/util/test.ts`

## Documentation

For detailed testing documentation including:
- How to write tests
- Test structure and patterns
- Current test coverage
- Best practices

See: `apps/docs/src/testing.md` or run `pnpm run start:docs` and visit the "Testing Guide" section.

## Test Status

✅ **Unit Tests**: Working - 1 test passing
✅ **E2E Test Infrastructure**: Fixed - tests load and run correctly
⏸️ **E2E Test Coverage**: Limited - most tests are skipped and need implementation

The E2E test infrastructure is now properly configured. Tests can run successfully but most test scenarios are marked as `it.skip()` and need to be implemented.
9 changes: 9 additions & 0 deletions apps/api/config/test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"authentication": {
"dlrg": {
"issuer": "https://test-issuer.example.com",
"clientId": "test-client-id",
"clientSecret": "test-client-secret"
}
}
}
2 changes: 2 additions & 0 deletions apps/api/src/routes/exports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { makeApp } from '../../util/make-app.js'
import { authorize } from './middleware/authorize.js'
import { veranstaltungPhotoArchive } from './photos.archive.js'
import { veranstaltungTeilnehmendenliste } from './teilnehmendenliste.sheet.js'
import { veranstaltungUnterschriftenliste } from './unterschriftenliste.sheet.js'
import { veranstaltungVerpflegung } from './verpflegung.sheet.js'

const exportRouter = makeApp()
.use(authorize)
.get('/sheet/teilnehmendenliste', veranstaltungTeilnehmendenliste)
.get('/sheet/unterschriftenliste', veranstaltungUnterschriftenliste)
.get('/sheet/verpflegung', veranstaltungVerpflegung)
.get('/archive/photos', veranstaltungPhotoArchive)

Expand Down
126 changes: 126 additions & 0 deletions apps/api/src/routes/exports/unterschriftenliste.sheet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import XLSX from '@e965/xlsx'
import dayjs from 'dayjs'
import type { Context } from 'hono'
import prisma from '../../prisma.js'
import { getSecurityWorksheet } from './helpers/getSecurityWorksheet.js'
import { getWorkbookDefaultProps } from './helpers/getWorkbookDefaultProps.js'
import type { AuthorizeResults } from './middleware/authorize.js'

export async function veranstaltungUnterschriftenliste(ctx: Context<{ Variables: AuthorizeResults }>) {
const { query, account, gliederung } = ctx.var

const anmeldungenList = await prisma.anmeldung.findMany({
where: {
OR: [
{
unterveranstaltungId: query.unterveranstaltungId,
},
{
unterveranstaltung: {
veranstaltungId: query.veranstaltungId,
},
},
],
unterveranstaltung: {
gliederungId: gliederung?.id,
},
status: 'BESTAETIGT',
},
select: {
id: true,
person: {
select: {
firstname: true,
lastname: true,
birthday: true,
address: {
select: {
street: true,
streetNumber: true,
zip: true,
city: true,
},
},
gliederung: {
select: {
name: true,
},
},
},
},
unterveranstaltung: {
select: {
beschreibung: true,
gliederung: {
select: {
name: true,
},
},
veranstaltung: {
select: {
name: true,
},
},
},
},
},
orderBy: {
person: {
lastname: 'asc',
},
},
})

const rows = anmeldungenList.map((anmeldung, index) => {
return {
'Nr.': index + 1,
Vorname: anmeldung.person.firstname,
Name: anmeldung.person.lastname,
'Straße, Hausnummer':
(anmeldung.person.address?.street ?? '') + ' ' + (anmeldung.person.address?.streetNumber ?? ''),
PLZ: anmeldung.person.address?.zip ?? '',
Ort: anmeldung.person.address?.city ?? '',
Geburtsdatum: anmeldung.person.birthday ? dayjs(anmeldung.person.birthday).format('DD.MM.YYYY') : '',
Gliederung: anmeldung.person.gliederung?.name ?? anmeldung.unterveranstaltung.gliederung.name,
Unterschrift: '',
}
})

const workbook = XLSX.utils.book_new()

/* get workbook defaults */
const defaultWorkbookProps = getWorkbookDefaultProps(account)
workbook.Props = {
Title: 'Unterschriftenliste',
Subject: 'Unterschriftenliste der Teilnehmenden',
...defaultWorkbookProps,
}

const worksheet = XLSX.utils.json_to_sheet(rows)

// Set column widths for better readability
worksheet['!cols'] = [
{ wch: 5 }, // Nr.
{ wch: 15 }, // Vorname
{ wch: 15 }, // Name
{ wch: 25 }, // Straße, Hausnummer
{ wch: 8 }, // PLZ
{ wch: 15 }, // Ort
{ wch: 12 }, // Geburtsdatum
{ wch: 20 }, // Gliederung
{ wch: 20 }, // Unterschrift
]

XLSX.utils.book_append_sheet(workbook, worksheet, `Unterschriftenliste`)

/** add Security Worksheet */
const { securityWorksheet, securityWorksheetName } = getSecurityWorksheet(account, rows.length)
XLSX.utils.book_append_sheet(workbook, securityWorksheet, securityWorksheetName)

const filename = `${dayjs().format('YYYYMMDD-HHmm')}-Unterschriftenliste.xlsx`
const buf = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }) as ArrayBuffer

ctx.header('Content-Disposition', `attachment; filename="${filename}"`)
ctx.header('Content-Type', 'application/vnd.ms-excel')
return ctx.body(buf, 200)
}
7 changes: 7 additions & 0 deletions apps/api/src/services/veranstaltung/veranstaltung.list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ export const veranstaltungSelect: Prisma.VeranstaltungSelect = {
id: true,
},
},
beschreibung: true,
datenschutz: true,
teilnahmeBedingungen: true,
teilnahmeBedingungenPublic: true,
zielgruppe: true,
meldebeginn: true,
meldeschluss: true,
maxTeilnehmende: true,
Expand All @@ -33,6 +38,8 @@ export const unterveranstaltungSelect: Prisma.UnterveranstaltungSelect = {
teilnahmegebuehr: true,
meldebeginn: true,
meldeschluss: true,
beschreibung: true,
bedingungen: true,
gliederungId: true,
_count: {
select: {
Expand Down
20 changes: 20 additions & 0 deletions apps/api/src/util/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,26 @@ interface Opts {
subjectId?: string
}

/**
* Logs an activity to the database for audit trail purposes.
* This function creates an activity record that tracks user actions and system events.
* If the logging fails, it logs a warning but doesn't throw an error to prevent disrupting the main flow.
* @param opts - The activity options
* @param opts.type - The type of activity (from ActivityType enum)
* @param opts.description - Optional description of the activity
* @param opts.causerId - Optional ID of the user/account who caused the activity
* @param opts.metadata - Optional metadata object with additional information
* @param opts.subjectType - The type of the subject entity (e.g., 'User', 'Event')
* @param opts.subjectId - Optional ID of the subject entity
* @example
* await logActivity({
* type: ActivityType.CREATE,
* description: 'Created new event',
* causerId: '123',
* subjectType: 'Event',
* subjectId: '456'
* })
*/
export default async function logActivity(opts: Opts) {
try {
await prisma.activity.create({
Expand Down
16 changes: 16 additions & 0 deletions apps/api/src/util/casing.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
/**
* Converts a PascalCase string to camelCase.
* @param str - The PascalCase string to convert
* @returns The string in camelCase format
* @example
* pascalToCamelCase('HelloWorld') // 'helloWorld'
* pascalToCamelCase('') // ''
*/
export function pascalToCamelCase(str: string) {
if (str.length === 0 || !str[0]) return str
return str[0].toLowerCase() + str.slice(1)
}

/**
* Converts the first character of a string to uppercase (converts to PascalCase).
* @param str - The string to convert
* @returns The string with the first character in uppercase
* @example
* toPascalCase('helloWorld') // 'HelloWorld'
* toPascalCase('foo') // 'Foo'
*/
export function toPascalCase(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
21 changes: 21 additions & 0 deletions apps/api/src/util/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,23 @@ import type { PathLike } from 'fs'
import { access, constants, lstat, readdir } from 'fs/promises'
import path from 'path'

/**
* Checks if a given path is a directory.
* @param source - The path to check
* @returns True if the path is a directory, false otherwise
* @internal
*/
async function isDirectory(source: string) {
return (await lstat(source)).isDirectory()
}

/**
* Gets all subdirectories within a given directory.
* @param source - The path to the parent directory
* @returns An array of directory names (not full paths)
* @example
* await getDirectories('/home/user/projects') // ['project1', 'project2']
*/
export async function getDirectories(source: string) {
const files = await readdir(source)
const dirs = await Promise.all(
Expand All @@ -16,6 +29,14 @@ export async function getDirectories(source: string) {
)
return dirs.filter((dir) => dir !== null)
}

/**
* Checks if a file or directory exists at the given path.
* @param file - The path to check
* @returns A promise that resolves to true if the file exists, false otherwise
* @example
* await checkFileExists('/path/to/file.txt') // true or false
*/
export function checkFileExists(file: PathLike) {
return access(file, constants.F_OK)
.then(() => true)
Expand Down
16 changes: 16 additions & 0 deletions apps/api/src/util/is-production.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
/**
* Checks if the application is running in production mode.
* @returns True if NODE_ENV is set to 'production', false otherwise
* @example
* if (isProduction()) {
* // Use production settings
* }
*/
export function isProduction(): boolean {
return process.env.NODE_ENV === 'production'
}

/**
* Checks if the application is running in development mode.
* @returns True if NODE_ENV is not set to 'production', false otherwise
* @example
* if (isDevelopment()) {
* // Enable debug logging
* }
*/
export function isDevelopment(): boolean {
return !isProduction()
}
Loading
Loading