From 42e97e5c3cf5a996cb2aeed89e012bf27e31e86b Mon Sep 17 00:00:00 2001 From: Axel Rindle Date: Mon, 9 Feb 2026 10:09:50 +0100 Subject: [PATCH 01/21] feat: display own title in table --- .../src/components/data/UnterveranstaltungenTable.vue | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/frontend/src/components/data/UnterveranstaltungenTable.vue b/apps/frontend/src/components/data/UnterveranstaltungenTable.vue index 9a7e918c..f515c534 100644 --- a/apps/frontend/src/components/data/UnterveranstaltungenTable.vue +++ b/apps/frontend/src/components/data/UnterveranstaltungenTable.vue @@ -13,6 +13,7 @@ import type { Option } from '../BasicInputs/BasicSelect.vue' import DataTable, { type Query } from '../Table/DataTable.vue' import initialData from '../Table/initialData' import Badge from '../UIComponents/Badge.vue' +import DataGridDoubleLineCell from '../DataGridDoubleLineCell.vue' const props = defineProps<{ veranstaltungId?: string @@ -60,6 +61,12 @@ const columns = [ type: 'text', }, }, + cell({ row }) { + return h(DataGridDoubleLineCell, { + title: row.original.gliederung.name, + message: row.original.beschreibung ?? '', + }) + }, }), column.accessor('type', { id: 'type', From 953977880c66e3d1b6cfa210697aa0e7827b0059 Mon Sep 17 00:00:00 2001 From: danielswiatek Date: Tue, 10 Feb 2026 21:27:42 +0100 Subject: [PATCH 02/21] feat: add additional fields to veranstaltungSelect for enhance d data retrieval #401 #closed --- apps/api/src/services/veranstaltung/veranstaltung.list.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/api/src/services/veranstaltung/veranstaltung.list.ts b/apps/api/src/services/veranstaltung/veranstaltung.list.ts index fc63da28..0c6e97fd 100644 --- a/apps/api/src/services/veranstaltung/veranstaltung.list.ts +++ b/apps/api/src/services/veranstaltung/veranstaltung.list.ts @@ -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, From 0a0c5a6ae597c9124d6c87c59d3d25c8a632a24c Mon Sep 17 00:00:00 2001 From: danielswiatek Date: Tue, 10 Feb 2026 21:46:13 +0100 Subject: [PATCH 03/21] feat: add beschreibung and bedingungen fields to unterveranstaltungSelect #401 --- apps/api/src/services/veranstaltung/veranstaltung.list.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/api/src/services/veranstaltung/veranstaltung.list.ts b/apps/api/src/services/veranstaltung/veranstaltung.list.ts index 0c6e97fd..710f6765 100644 --- a/apps/api/src/services/veranstaltung/veranstaltung.list.ts +++ b/apps/api/src/services/veranstaltung/veranstaltung.list.ts @@ -38,6 +38,8 @@ export const unterveranstaltungSelect: Prisma.UnterveranstaltungSelect = { teilnahmegebuehr: true, meldebeginn: true, meldeschluss: true, + beschreibung: true, + bedingungen: true, gliederungId: true, _count: { select: { From 7d8e824f4bb1b4fdc23b3ee23b8ba4d5a179afa2 Mon Sep 17 00:00:00 2001 From: danielswiatek Date: Tue, 10 Feb 2026 22:12:57 +0100 Subject: [PATCH 04/21] fix: remove unnecessary line and ensure isDeleting is accessed correctly #400 #closed --- .../src/components/CustomFields/CustomFieldDeleteModal.vue | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/frontend/src/components/CustomFields/CustomFieldDeleteModal.vue b/apps/frontend/src/components/CustomFields/CustomFieldDeleteModal.vue index 9b0444b4..fc36bdec 100644 --- a/apps/frontend/src/components/CustomFields/CustomFieldDeleteModal.vue +++ b/apps/frontend/src/components/CustomFields/CustomFieldDeleteModal.vue @@ -30,7 +30,6 @@ const { if (!props.field) { return } - switch (props.entity) { case 'veranstaltung': await apiClient.customFields.veranstaltungDelete.mutate({ @@ -56,10 +55,9 @@ const { defineExpose({ show() { - if (isDeleting) { + if (isDeleting.value) { return } - modal.value?.show() }, hide() { From 3fbccdb610e4f717fcabd17feef243529a6acb7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:21:45 +0000 Subject: [PATCH 05/21] Initial plan From 549b377fc7dc9fa6b3882d355b8b829f5367d75e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:27:08 +0000 Subject: [PATCH 06/21] Fix E2E test configuration and dependencies Co-authored-by: danielswiatek <9080990+danielswiatek@users.noreply.github.com> --- apps/api/config/test.json | 9 +++++++ apps/frontend/package.json | 2 ++ apps/frontend/vitest.config.e2e.ts | 27 ++++++++++++++++----- pnpm-lock.yaml | 38 ++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 apps/api/config/test.json diff --git a/apps/api/config/test.json b/apps/api/config/test.json new file mode 100644 index 00000000..25f33bbd --- /dev/null +++ b/apps/api/config/test.json @@ -0,0 +1,9 @@ +{ + "authentication": { + "dlrg": { + "issuer": "https://test-issuer.example.com", + "clientId": "test-client-id", + "clientSecret": "test-client-secret" + } + } +} diff --git a/apps/frontend/package.json b/apps/frontend/package.json index c2fc50b6..d403063b 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -59,8 +59,10 @@ "@vitejs/plugin-basic-ssl": "^2.1.0", "@vitejs/plugin-vue": "^6.0.2", "autoprefixer": "^10.4.16", + "dayjs": "^1.11.19", "eslint": "catalog:", "http2-proxy": "^5.0.53", + "playwright": "^1.58.2", "postcss": "^8.4.31", "sass": "^1.69.5", "tailwindcss": "^3.3.5", diff --git a/apps/frontend/vitest.config.e2e.ts b/apps/frontend/vitest.config.e2e.ts index 74cd2f9d..15455031 100644 --- a/apps/frontend/vitest.config.e2e.ts +++ b/apps/frontend/vitest.config.e2e.ts @@ -1,8 +1,23 @@ // vitest.config.e2e.ts -import { defineConfig } from 'vitest/config' +import { defineConfig, mergeConfig } from 'vitest/config' +import viteConfig from './vite.config.js' -export default defineConfig({ - test: { - include: ['tests/**/*.e2e.ts'], - }, -}) +export default mergeConfig( + viteConfig, + defineConfig({ + test: { + include: ['tests/**/*.e2e.ts'], + globals: true, + testTimeout: 60000, + hookTimeout: 60000, + }, + resolve: { + alias: { + dayjs: 'dayjs', + }, + }, + optimizeDeps: { + include: ['dayjs', 'playwright'], + }, + }) +) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0f8c0d2..3b26806c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -389,12 +389,18 @@ importers: autoprefixer: specifier: ^10.4.16 version: 10.4.20(postcss@8.5.1) + dayjs: + specifier: ^1.11.19 + version: 1.11.19 eslint: specifier: 'catalog:' version: 9.19.0(jiti@1.21.7) http2-proxy: specifier: ^5.0.53 version: 5.0.53 + playwright: + specifier: ^1.58.2 + version: 1.58.2 postcss: specifier: ^8.4.31 version: 8.5.1 @@ -3040,6 +3046,9 @@ packages: dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} @@ -3381,6 +3390,11 @@ packages: fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3410,6 +3424,7 @@ packages: glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true globals@13.24.0: @@ -4140,6 +4155,16 @@ packages: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + engines: {node: '>=18'} + hasBin: true + postcss-import@15.1.0: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -7516,6 +7541,8 @@ snapshots: dayjs@1.11.13: {} + dayjs@1.11.19: {} + de-indent@1.0.2: {} debug@4.4.0: @@ -7913,6 +7940,9 @@ snapshots: fraction.js@4.3.7: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -8827,6 +8857,14 @@ snapshots: pirates@4.0.6: {} + playwright-core@1.58.2: {} + + playwright@1.58.2: + dependencies: + playwright-core: 1.58.2 + optionalDependencies: + fsevents: 2.3.2 + postcss-import@15.1.0(postcss@8.5.1): dependencies: postcss: 8.5.1 From 3a9bfe4af4aa770a96b39088867da4158d9c45b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:29:13 +0000 Subject: [PATCH 07/21] Add comprehensive testing documentation Co-authored-by: danielswiatek <9080990+danielswiatek@users.noreply.github.com> --- TESTING.md | 62 +++++++ apps/docs/.vitepress/config.mts | 15 ++ apps/docs/src/testing.md | 297 ++++++++++++++++++++++++++++++++ apps/frontend/tests/README.md | 107 ++++++++++++ 4 files changed, 481 insertions(+) create mode 100644 TESTING.md create mode 100644 apps/docs/src/testing.md create mode 100644 apps/frontend/tests/README.md diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 00000000..8b7f6f75 --- /dev/null +++ b/TESTING.md @@ -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. diff --git a/apps/docs/.vitepress/config.mts b/apps/docs/.vitepress/config.mts index ab8b5ea2..7e234b26 100644 --- a/apps/docs/.vitepress/config.mts +++ b/apps/docs/.vitepress/config.mts @@ -13,6 +13,21 @@ export default defineConfig({ ], sidebar: [ + { + text: 'Development', + items: [ + { text: 'Testing Guide', link: '/testing' }, + { text: 'App Structure', link: '/app-structure' }, + ], + }, + { + text: 'Domain Knowledge', + items: [ + { text: 'Anmelde Entity', link: '/anmelde-entity' }, + { text: 'Qualifikationen', link: '/qualifikationen' }, + { text: 'Bugs and Workarounds', link: '/bugs-and-workarounds' }, + ], + }, { text: 'Examples', items: [ diff --git a/apps/docs/src/testing.md b/apps/docs/src/testing.md new file mode 100644 index 00000000..52bb21f4 --- /dev/null +++ b/apps/docs/src/testing.md @@ -0,0 +1,297 @@ +# Testing Guide + +This document explains how to run and write tests for brahmsee.digital. + +## Overview + +The project uses two types of tests: + +1. **Unit Tests** - Fast, isolated tests for individual functions and utilities +2. **End-to-End (E2E) Tests** - Integration tests that test the entire application flow using a real browser + +## Test Framework + +- **Vitest** - Modern test runner (compatible with Vite) +- **Playwright** - Browser automation for E2E tests +- **Testing Philosophy** - We use Vitest with Playwright for E2E tests instead of native Playwright Test to keep test infrastructure unified + +## Running Tests + +### Unit Tests + +Unit tests are fast and can run without any external dependencies: + +```bash +# Run all unit tests +pnpm test + +# Run unit tests in watch mode +pnpm test --watch + +# Run unit tests with coverage +pnpm test --coverage +``` + +### E2E Tests + +E2E tests require additional infrastructure to run: + +#### Prerequisites + +1. **Install Playwright browsers** (one-time setup): + ```bash + cd apps/frontend + pnpm exec playwright install + pnpm exec playwright install-deps + ``` + +2. **Start required services**: + ```bash + # Start database and other services + pnpm run start:services + + # In another terminal, start the API + pnpm run start:api + + # In another terminal, start the frontend + pnpm run start:frontend + ``` + +3. **Run E2E tests**: + ```bash + cd apps/frontend + NODE_ENV=test pnpm exec vitest --run -c ./vitest.config.e2e.ts + ``` + +## Writing Tests + +### Unit Tests + +Unit tests should be placed alongside the code they test with a `.test.ts` extension. + +**Example**: `packages/helpers/src/group-by.test.ts` + +```typescript +import { expect, test } from 'vitest' +import { groupBy } from './group-by.js' + +test('groupBy', () => { + const data = [ + { id: 1, category: 'A' }, + { id: 2, category: 'B' }, + ] + + const result = groupBy(data, (item) => item.category) + + expect(result).toEqual({ + A: [{ id: 1, category: 'A' }], + B: [{ id: 2, category: 'B' }], + }) +}) +``` + +### E2E Tests + +E2E tests are located in `apps/frontend/tests/` and have a `.e2e.ts` extension. + +**Structure of an E2E test:** + +```typescript +import dayjs from 'dayjs' +import { type Browser, chromium, type BrowserContext } from 'playwright' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import * as testUtils from '@codeanker/api/test' + +describe('Feature: Login', () => { + let browser: Browser + let context: BrowserContext + let data: Awaited> + const runId = (Math.random() + 1).toString(36).substring(7) + + beforeAll(async () => { + // Launch browser + browser = await chromium.launch({ headless: true }) + context = await browser.newContext({ ignoreHTTPSErrors: true }) + + // Create test data in database + data = await testUtils.createMock(runId) + }) + + afterAll(async () => { + // Clean up browser + browser?.close() + + // Clean up test data from database + await testUtils.cleanup(runId) + }) + + it('User can login', async () => { + const page = await context.newPage() + await page.goto('https://localhost:8080/login') + await page.getByPlaceholder('E-Mail').fill(data.account.email) + await page.getByPlaceholder('Passwort').fill(data.accountPassword) + await page.getByPlaceholder('Passwort').press('Enter') + + // Wait for navigation away from login page + await expect(page).not.toHaveURL(/login/) + }) +}) +``` + +### Helper Functions + +#### insertJwtToken + +For tests that don't need to test the login flow, you can directly insert a JWT token: + +```typescript +import { insertJwtToken } from './helpers/insertJwtToken' + +it('Can access protected page', async () => { + const page = await context.newPage() + + // Insert JWT token and last event ID into localStorage + await insertJwtToken(page, data.accessToken, data.veranstaltung.id) + + // Navigate to protected page + await page.goto('https://localhost:8080/verwaltung/accounts') + await page.waitForLoadState('networkidle') + + // Assert page loaded successfully + await expect(page).toHaveURL(/verwaltung/) +}) +``` + +## Current Test Coverage + +### Unit Tests +- ✅ `packages/helpers/src/group-by.test.ts` - Tests the groupBy utility function + +### E2E Tests + +#### Login & Authentication +- ✅ `tests/login.e2e.ts` - User login functionality + - ✅ Standard user can login + - ⏸️ Password reset (skipped - not implemented yet) + +#### Event Management (Veranstaltung) +- ⏸️ `tests/veranstaltung-dashboard.e2e.ts` - Event dashboard view +- ⏸️ `tests/veranstaltung-anmeldungen.e2e.ts` - Event registrations +- ⏸️ `tests/veranstaltung-auswertung.e2e.ts` - Event evaluations +- ⏸️ `tests/veranstaltung-lageplan.e2e.ts` - Event site map +- ⏸️ `tests/veranstaltung-programm.e2e.ts` - Event program + +#### Administration (Verwaltung) +- ✅ `tests/verwaltung-benutzer.e2e.ts` - User management + - ✅ View is accessible +- ✅ `tests/verwaltung-gliederungen.e2e.ts` - Organization management + - ✅ List organizations + - ⏸️ Create organization (skipped) + - ⏸️ View organization details (skipped) +- ⏸️ `tests/verwaltung-orte.e2e.ts` - Location management +- ⏸️ `tests/verwaltung-veranstaltungen.e2e.ts` - Event administration + +Legend: +- ✅ = Test implemented and active +- ⏸️ = Test exists but is skipped (needs implementation) + +## Test Infrastructure + +### Why Vitest + Playwright? + +Originally, we used Vitest with Playwright to unify unit tests and E2E tests under one test runner. While native Playwright Test is more common for E2E testing, we continue to use this approach because: + +1. **Unified Configuration** - Single test configuration for both unit and E2E tests +2. **Consistent Developer Experience** - Same commands and patterns for all tests +3. **Vite Integration** - Seamless integration with our Vite-based frontend build + +### Test Configuration Files + +- `vitest.config.ts` - Root configuration for unit tests +- `vitest.workspace.ts` - Workspace configuration (currently only using root config) +- `apps/frontend/vitest.config.e2e.ts` - Configuration for E2E tests +- `apps/api/config/test.json` - API configuration for test environment + +### Test Utilities + +Located in `apps/api/src/util/test.ts`: + +- `createMock(runId)` - Creates a test user account and returns credentials +- `cleanup(runId)` - Removes test data created by createMock + +## Common Issues + +### Playwright browsers not installed + +``` +Error: browserType.launch: Executable doesn't exist +``` + +**Solution**: Install Playwright browsers: +```bash +cd apps/frontend +pnpm exec playwright install +``` + +### Database not accessible + +``` +PrismaClientInitializationError: Can't reach database server +``` + +**Solution**: Start the database service: +```bash +pnpm run start:services +``` + +### Configuration errors in tests + +``` +ZodError: Required field missing +``` + +**Solution**: Ensure `apps/api/config/test.json` exists with required configuration values + +## Test Scenarios to Implement + +The following test scenarios have been identified but not yet implemented: + +### High Priority +1. **Event Registration Flow** - Users registering for events +2. **Event Creation** - Administrators creating new events +3. **User Management** - Creating and editing user accounts +4. **Organization Management** - CRUD operations for organizations + +### Medium Priority +5. **Location Management** - Adding and editing event locations +6. **Event Program** - Managing event schedules +7. **Site Map** - Event location maps +8. **Event Evaluation** - Post-event analytics and reports + +### Low Priority +9. **Password Reset Flow** - Complete password reset functionality +10. **Bulk Operations** - Mass updates and exports + +## Best Practices + +1. **Use unique test data** - Generate random IDs (`runId`) to avoid conflicts +2. **Clean up after tests** - Always remove test data in `afterAll` +3. **Test user journeys** - E2E tests should simulate real user workflows +4. **Keep tests independent** - Tests should not depend on each other +5. **Use meaningful descriptions** - Write test names in German to match the UI +6. **Skip incomplete tests** - Use `it.skip` for tests that are not yet implemented + +## Contributing + +When adding new features, please: + +1. Add unit tests for new utility functions +2. Add E2E tests for new user-facing features +3. Update this documentation with new test scenarios +4. Ensure all tests pass before submitting a PR + +## Resources + +- [Vitest Documentation](https://vitest.dev/) +- [Playwright Documentation](https://playwright.dev/) +- [Testing Best Practices](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library) diff --git a/apps/frontend/tests/README.md b/apps/frontend/tests/README.md new file mode 100644 index 00000000..aced4869 --- /dev/null +++ b/apps/frontend/tests/README.md @@ -0,0 +1,107 @@ +# E2E Tests + +This directory contains end-to-end tests for the brahmsee.digital frontend application. + +## Quick Start + +### Prerequisites + +1. Install Playwright browsers (one-time setup): + ```bash + pnpm exec playwright install + pnpm exec playwright install-deps + ``` + +2. Start required services: + ```bash + # From repository root + pnpm run start:services # Database and other services + pnpm run start:api # API server (in another terminal) + pnpm run start:frontend # Frontend dev server (in another terminal) + ``` + +### Running Tests + +```bash +# From repository root or apps/frontend directory +NODE_ENV=test pnpm exec vitest --run -c ./apps/frontend/vitest.config.e2e.ts +``` + +## Test Structure + +Each test file follows this pattern: + +```typescript +describe('View: ', () => { + let browser: Browser + let context: BrowserContext + let data: Awaited> + const runId = (Math.random() + 1).toString(36).substring(7) + + beforeAll(async () => { + // Setup: Launch browser and create test data + browser = await chromium.launch({ headless: true }) + context = await browser.newContext({ ignoreHTTPSErrors: true }) + data = await testUtils.createMock(runId) + }) + + afterAll(async () => { + // Cleanup: Close browser and remove test data + browser?.close() + await testUtils.cleanup(runId) + }) + + it('test description', async () => { + // Test implementation + }) +}) +``` + +## Current Tests + +### Active Tests (Not Skipped) +- `login.e2e.ts` - Login functionality +- `verwaltung-benutzer.e2e.ts` - User management view +- `verwaltung-gliederungen.e2e.ts` - Organization management + +### Skipped Tests (Need Implementation) +Most tests are currently skipped with `it.skip()` and need to be implemented: +- Event dashboard, registrations, evaluations, site map, program +- Location and event administration + +## Helper Functions + +### insertJwtToken +Located in `helpers/insertJwtToken.ts`, this function allows tests to bypass the login flow by directly injecting a JWT token into localStorage: + +```typescript +await insertJwtToken(page, data.accessToken, data.veranstaltung.id) +``` + +## Test Utilities + +The `@codeanker/api/test` module provides: + +- `createMock(runId)` - Creates test user with admin role +- `cleanup(runId)` - Removes test data from database + +## Common Issues + +### "Executable doesn't exist" error +Run `pnpm exec playwright install` to download browsers. + +### "Can't reach database" error +Ensure `pnpm run start:services` is running. + +### Tests timing out +Increase timeout in `vitest.config.e2e.ts` or check that services are running. + +## Documentation + +For complete testing documentation, see `/apps/docs/src/testing.md` or run the docs: + +```bash +pnpm run start:docs +``` + +Then visit the "Testing Guide" in the sidebar. From cf28d94663abd170e710351c0eb0638e543cf103 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:30:42 +0000 Subject: [PATCH 08/21] Remove redundant alias configuration from vitest config Co-authored-by: danielswiatek <9080990+danielswiatek@users.noreply.github.com> --- apps/frontend/vitest.config.e2e.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/apps/frontend/vitest.config.e2e.ts b/apps/frontend/vitest.config.e2e.ts index 15455031..eabb3dfb 100644 --- a/apps/frontend/vitest.config.e2e.ts +++ b/apps/frontend/vitest.config.e2e.ts @@ -11,13 +11,5 @@ export default mergeConfig( testTimeout: 60000, hookTimeout: 60000, }, - resolve: { - alias: { - dayjs: 'dayjs', - }, - }, - optimizeDeps: { - include: ['dayjs', 'playwright'], - }, }) ) From ffd2480cc9cbf51370662e1d7621ca3c459da96f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:35:25 +0000 Subject: [PATCH 09/21] Initial plan From 8404548809121a3c8015402d6c694ad20d008b3b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:39:05 +0000 Subject: [PATCH 10/21] Add signature list export functionality Co-authored-by: danielswiatek <9080990+danielswiatek@users.noreply.github.com> --- apps/api/src/routes/exports/index.ts | 2 + .../exports/unterschriftenliste.sheet.ts | 124 ++++++++++++++++++ .../UnterveranstaltungDetail.vue | 8 ++ .../Veranstaltungen/VeranstaltungDetail.vue | 8 ++ 4 files changed, 142 insertions(+) create mode 100644 apps/api/src/routes/exports/unterschriftenliste.sheet.ts diff --git a/apps/api/src/routes/exports/index.ts b/apps/api/src/routes/exports/index.ts index 54b550f9..69a8e49a 100644 --- a/apps/api/src/routes/exports/index.ts +++ b/apps/api/src/routes/exports/index.ts @@ -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) diff --git a/apps/api/src/routes/exports/unterschriftenliste.sheet.ts b/apps/api/src/routes/exports/unterschriftenliste.sheet.ts new file mode 100644 index 00000000..3cf22118 --- /dev/null +++ b/apps/api/src/routes/exports/unterschriftenliste.sheet.ts @@ -0,0 +1,124 @@ +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, + 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 ?? '', + 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, 201) +} diff --git a/apps/frontend/src/views/Unterveranstaltung/UnterveranstaltungDetail.vue b/apps/frontend/src/views/Unterveranstaltung/UnterveranstaltungDetail.vue index f985d383..66e3ac73 100644 --- a/apps/frontend/src/views/Unterveranstaltung/UnterveranstaltungDetail.vue +++ b/apps/frontend/src/views/Unterveranstaltung/UnterveranstaltungDetail.vue @@ -142,6 +142,14 @@ const files: ExportedFileType[] = [ bgColor: 'bg-blue-600', hoverColor: 'hover:text-blue-700', }, + { + name: 'Unterschriftenliste', + icon: DocumentDuplicateIcon, + href: `/api/export/sheet/unterschriftenliste?${exportParams}`, + description: 'Unterschriftenliste für Förderungen', + bgColor: 'bg-green-600', + hoverColor: 'hover:text-green-700', + }, { name: 'Verpflegung', initial: 'VP', diff --git a/apps/frontend/src/views/Verwaltung/Veranstaltungen/VeranstaltungDetail.vue b/apps/frontend/src/views/Verwaltung/Veranstaltungen/VeranstaltungDetail.vue index e7ea5a5b..a64b9a9a 100644 --- a/apps/frontend/src/views/Verwaltung/Veranstaltungen/VeranstaltungDetail.vue +++ b/apps/frontend/src/views/Verwaltung/Veranstaltungen/VeranstaltungDetail.vue @@ -102,6 +102,14 @@ const files: ExportedFileType[] = [ bgColor: 'bg-blue-600', hoverColor: 'hover:text-blue-700', }, + { + name: 'Unterschriftenliste', + icon: DocumentDuplicateIcon, + href: `/api/export/sheet/unterschriftenliste?${exportParams}`, + description: 'Unterschriftenliste für Förderungen', + bgColor: 'bg-green-600', + hoverColor: 'hover:text-green-700', + }, { name: 'Verpflegung', initial: 'VP', From 97b490faff7c10bf7a427081776ce1b15fee1427 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:41:28 +0000 Subject: [PATCH 11/21] Fix HTTP status code for signature list export endpoint Co-authored-by: danielswiatek <9080990+danielswiatek@users.noreply.github.com> --- apps/api/src/routes/exports/unterschriftenliste.sheet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/routes/exports/unterschriftenliste.sheet.ts b/apps/api/src/routes/exports/unterschriftenliste.sheet.ts index 3cf22118..2ba9e131 100644 --- a/apps/api/src/routes/exports/unterschriftenliste.sheet.ts +++ b/apps/api/src/routes/exports/unterschriftenliste.sheet.ts @@ -120,5 +120,5 @@ export async function veranstaltungUnterschriftenliste(ctx: Context<{ Variables: ctx.header('Content-Disposition', `attachment; filename="${filename}"`) ctx.header('Content-Type', 'application/vnd.ms-excel') - return ctx.body(buf, 201) + return ctx.body(buf, 200) } From a4ebe6d936127aa07ff33c2204c8cfa63c63a94d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:41:43 +0000 Subject: [PATCH 12/21] Initial plan From 7132465131cddd57ed72dea7c7e9823f87f8cfa2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:44:16 +0000 Subject: [PATCH 13/21] Add JSDoc to packages/helpers utility functions Co-authored-by: danielswiatek <9080990+danielswiatek@users.noreply.github.com> --- packages/helpers/src/dayjs.ts | 13 ++++++++++++ packages/helpers/src/format.ts | 24 +++++++++++++++++++++++ packages/helpers/src/formatBytes.ts | 10 ++++++++++ packages/helpers/src/formatCurrency.ts | 18 +++++++++++++++++ packages/helpers/src/password-strength.ts | 24 +++++++++++++++++++++++ 5 files changed, 89 insertions(+) diff --git a/packages/helpers/src/dayjs.ts b/packages/helpers/src/dayjs.ts index c424b798..5b386e8d 100644 --- a/packages/helpers/src/dayjs.ts +++ b/packages/helpers/src/dayjs.ts @@ -1,3 +1,16 @@ +/** + * Pre-configured dayjs instance with German locale and Europe/Berlin timezone. + * + * This module extends dayjs with: + * - Custom parse format support + * - UTC and timezone support + * - German locale (de) + * - Default timezone set to Europe/Berlin + * + * @module dayjs + * @see {@link https://day.js.org/docs/en/installation/installation|Day.js Documentation} + */ + import dayjs, { type Dayjs } from 'dayjs' import customParseFormat from 'dayjs/plugin/customParseFormat.js' import 'dayjs/locale/de' diff --git a/packages/helpers/src/format.ts b/packages/helpers/src/format.ts index f2dd33ad..be9a7ae4 100644 --- a/packages/helpers/src/format.ts +++ b/packages/helpers/src/format.ts @@ -1,9 +1,25 @@ import { dayjs } from './dayjs.js' +/** + * Formats a date using a custom format string. + * @param date - The date to format (can be a Date object, string, or any dayjs-compatible type) + * @param format - The format string (e.g., 'DD.MM.YYYY', 'YYYY-MM-DD HH:mm:ss') + * @returns The formatted date string + * @example + * formatDateWith(new Date(), 'DD.MM.YYYY') // '10.02.2026' + */ export function formatDateWith(date: dayjs.ConfigType, format: string): string { return dayjs(date).format(format) } +/** + * Formats a date in the German standard format (DD.MM.YYYY). + * @param date - The date to format (can be a Date object, string, or any dayjs-compatible type) + * @returns The formatted date string in DD.MM.YYYY format, or an empty string if date is falsy + * @example + * formatDate(new Date('2026-02-10')) // '10.02.2026' + * formatDate(null) // '' + */ export function formatDate(date: dayjs.ConfigType): string { if (!date) { return '' @@ -12,6 +28,14 @@ export function formatDate(date: dayjs.ConfigType): string { return formatDateWith(date, 'DD.MM.YYYY') } +/** + * Formats a date with time in the German standard format (DD.MM.YYYY HH:mm:ss). + * @param date - The date to format (can be a Date object, string, or any dayjs-compatible type) + * @returns The formatted timestamp string in DD.MM.YYYY HH:mm:ss format, or an empty string if date is falsy + * @example + * formatTimestamp(new Date('2026-02-10T21:42:00')) // '10.02.2026 21:42:00' + * formatTimestamp(null) // '' + */ export function formatTimestamp(date: dayjs.ConfigType): string { if (!date) { return '' diff --git a/packages/helpers/src/formatBytes.ts b/packages/helpers/src/formatBytes.ts index 110ac41b..26610312 100644 --- a/packages/helpers/src/formatBytes.ts +++ b/packages/helpers/src/formatBytes.ts @@ -1,3 +1,13 @@ +/** + * Converts a byte value to a human-readable string with appropriate units. + * @param bytes - The number of bytes to format + * @param decimals - The number of decimal places to include (default: 2) + * @returns A formatted string with the value and unit (e.g., '1.5 MB', '500 Bytes') + * @example + * formatBytes(1024) // '1 KB' + * formatBytes(1536, 2) // '1.5 KB' + * formatBytes(0) // '0 Bytes' + */ export function formatBytes(bytes: number, decimals = 2) { if (!+bytes) return '0 Bytes' diff --git a/packages/helpers/src/formatCurrency.ts b/packages/helpers/src/formatCurrency.ts index 3b1f1f2c..a351a4e5 100644 --- a/packages/helpers/src/formatCurrency.ts +++ b/packages/helpers/src/formatCurrency.ts @@ -1,4 +1,15 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ +/** + * Formats a numeric value as currency in German format (EUR). + * @param value - The value to format (number or string that can be parsed to a number) + * @param round - If true, rounds to whole numbers; if false, keeps 2 decimal places (default: false) + * @param isCents - If true, treats the value as cents and divides by 100 (default: false) + * @returns The formatted currency string in German locale (e.g., '1.234,56 €') + * @example + * formatCurrency(1234.56) // '1.234,56 €' + * formatCurrency(12345, false, true) // '123,45 €' (value is in cents) + * formatCurrency('99.99') // '99,99 €' + */ export function formatCurrency(value, round = false, isCents = false) { if (typeof value !== 'number') { const parsedValue = Number.parseFloat(value) @@ -23,6 +34,13 @@ export function formatCurrency(value, round = false, isCents = false) { } } +/** + * Internal helper to fix the number of decimal places without rounding. + * @param num - The number to fix + * @param fixed - The number of decimal places to keep + * @returns The number with fixed decimal places + * @internal + */ function toFixed(num, fixed) { const re = new RegExp('^-?\\d+(?:.\\d{0,' + (fixed || -1) + '})?') // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access diff --git a/packages/helpers/src/password-strength.ts b/packages/helpers/src/password-strength.ts index 1721c536..38655466 100644 --- a/packages/helpers/src/password-strength.ts +++ b/packages/helpers/src/password-strength.ts @@ -8,6 +8,20 @@ const levels: ((password: string) => boolean)[] = [ const requiredPasswordLevel = levels.length +/** + * Computes the password strength level based on various criteria. + * The password is tested against multiple criteria: + * - At least 8 characters long + * - Contains lowercase letters + * - Contains uppercase letters + * - Contains numbers + * - Contains special characters + * @param password - The password string to evaluate + * @returns A number from 0 to 5 indicating the password strength level + * @example + * computePasswordLevel('weak') // 1 (only length criteria met) + * computePasswordLevel('Strong123!') // 5 (all criteria met) + */ export function computePasswordLevel(password: string) { let level = 0 @@ -22,6 +36,16 @@ export function computePasswordLevel(password: string) { return level } +/** + * Checks if a password meets all strength requirements. + * A password is considered strong if it meets all 5 criteria: + * length >= 8, lowercase, uppercase, numbers, and special characters. + * @param password - The password string to validate + * @returns True if the password meets all strength requirements, false otherwise + * @example + * isStrongPassword('weak') // false + * isStrongPassword('Strong123!') // true + */ export function isStrongPassword(password: string) { return computePasswordLevel(password) === requiredPasswordLevel } From 3d85dbbd670fdf9a1721c4186d5389ccf9178506 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:45:35 +0000 Subject: [PATCH 14/21] Add JSDoc to apps/api/src/util backend utilities Co-authored-by: danielswiatek <9080990+danielswiatek@users.noreply.github.com> --- apps/api/src/util/activity.ts | 20 ++++++++++ apps/api/src/util/casing.ts | 16 ++++++++ apps/api/src/util/files.ts | 21 +++++++++++ apps/api/src/util/is-production.ts | 16 ++++++++ apps/api/src/util/mail.ts | 59 ++++++++++++++++++++++++++++-- apps/api/src/util/zod.ts | 22 +++++++++-- 6 files changed, 146 insertions(+), 8 deletions(-) diff --git a/apps/api/src/util/activity.ts b/apps/api/src/util/activity.ts index 1080bad2..1af39733 100644 --- a/apps/api/src/util/activity.ts +++ b/apps/api/src/util/activity.ts @@ -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({ diff --git a/apps/api/src/util/casing.ts b/apps/api/src/util/casing.ts index e70191fe..e2614f2b 100644 --- a/apps/api/src/util/casing.ts +++ b/apps/api/src/util/casing.ts @@ -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) } diff --git a/apps/api/src/util/files.ts b/apps/api/src/util/files.ts index 0a9275f0..585c48c0 100644 --- a/apps/api/src/util/files.ts +++ b/apps/api/src/util/files.ts @@ -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( @@ -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) diff --git a/apps/api/src/util/is-production.ts b/apps/api/src/util/is-production.ts index f11d059b..61346027 100644 --- a/apps/api/src/util/is-production.ts +++ b/apps/api/src/util/is-production.ts @@ -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() } diff --git a/apps/api/src/util/mail.ts b/apps/api/src/util/mail.ts index d9565769..607fcf4e 100644 --- a/apps/api/src/util/mail.ts +++ b/apps/api/src/util/mail.ts @@ -49,6 +49,12 @@ type EMail = { }[] } +/** + * Reads an email template file from the template directory. + * @param name - The name of the template file (without .mjml extension) + * @returns An object containing the absolute path and contents of the template + * @internal + */ async function readTemplate(name: string) { const abs = `${join(templateDirectory, name)}.mjml` const buffer = await readFile(abs) @@ -66,10 +72,14 @@ const { contents: layout } = await readTemplate('_layout') Handlebars.registerPartial('layout', layout) /** - * Triggers the email template compilation pipeline consisting of the following: - * - * - Use `ejs` as a templating engine for enabling variable support. - * - Use `mjml` to generate the raw html code to embed in the email. + * Compiles an email template using Handlebars and MJML. + * The pipeline consists of: + * - Using Handlebars as a templating engine for variable support + * - Using MJML to generate the raw HTML code for the email + * @param templateName - The name of the template to compile + * @param variables - The variables to inject into the template + * @returns The compiled HTML string, or null if compilation fails + * @internal */ async function compile(templateName: string, variables: Variables): Promise { const { abs, contents } = await readTemplate(templateName) @@ -91,6 +101,29 @@ async function compile(templateName: string, variables: Variables): Promise = [true, O] | [false, ZodError] /** * Runs a zod schema validation and returns the result data as a tuple. - * - * @param schema The zod schema to use for validation. - * @param payload The payload to validate. - * @returns The validation result. + * @param schema - The zod schema to use for validation + * @param payload - The payload to validate + * @returns A tuple where the first element indicates success (true) or failure (false), + * and the second element contains either the validated data or the ZodError + * @example + * const [success, result] = await zodSafe(mySchema, data) + * if (success) { + * // result is the validated data + * } else { + * // result is a ZodError + * } */ export async function zodSafe(schema: ZodSchema, payload: I): Promise> { const { success, data, error } = await schema.safeParseAsync(payload) @@ -19,4 +26,11 @@ export async function zodSafe(schema: ZodSchema, payload: I): Promise v === 'true') From 5c79c0da0e9f0c7e6cb5727032eb5751294a464b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:46:36 +0000 Subject: [PATCH 15/21] Add JSDoc to packages/validation utilities Co-authored-by: danielswiatek <9080990+danielswiatek@users.noreply.github.com> --- packages/validation/src/defineRule.ts | 21 +++++++++++++++++++++ packages/validation/src/executeRules.ts | 10 ++++++++++ packages/validation/src/rules/confirm.ts | 12 ++++++++++++ packages/validation/src/rules/emailValid.ts | 10 ++++++++++ packages/validation/src/rules/required.ts | 16 ++++++++++++++++ 5 files changed, 69 insertions(+) diff --git a/packages/validation/src/defineRule.ts b/packages/validation/src/defineRule.ts index 50ea33d0..1ee8a063 100644 --- a/packages/validation/src/defineRule.ts +++ b/packages/validation/src/defineRule.ts @@ -1,8 +1,29 @@ +/** + * Context object passed to validation rule functions. + * @property name - The name of the field being validated + */ export type RuleFieldContext = { name: string } + +/** + * A validation rule function that returns true if valid, or an error message string if invalid. + * @param value - The value to validate + * @param context - Context information about the field being validated + * @returns True if the validation passes, or an error message string if it fails + */ export type RuleFunction = (value: unknown, context: RuleFieldContext) => true | string | Promise +/** + * Defines a validation rule function with proper type checking. + * This is a type-safe wrapper for creating validation rules. + * @param ruleFunction - The validation rule function to define + * @returns The same rule function with proper typing + * @example + * const myRule = defineRule((value, context) => { + * return value === 'expected' || `${context.name} must be 'expected'` + * }) + */ export function defineRule(ruleFunction: RuleFunction) { return ruleFunction } diff --git a/packages/validation/src/executeRules.ts b/packages/validation/src/executeRules.ts index 1a888e50..4ab3ab7c 100644 --- a/packages/validation/src/executeRules.ts +++ b/packages/validation/src/executeRules.ts @@ -2,6 +2,16 @@ import { type ComputedRef } from 'vue' import type { RuleFieldContext, RuleFunction } from './defineRule.js' +/** + * Executes a list of validation rules against a value. + * Returns the first error message encountered, or true if all rules pass. + * @param rules - A computed ref containing the array of rule functions to execute + * @returns An async function that validates a value against all rules + * @example + * const validator = excecuteRules(computed(() => [required(true), emailValid])) + * const result = await validator('test@example.com', { name: 'email' }) + * // result is true if valid, or an error message string if invalid + */ export function excecuteRules(rules: ComputedRef) { return async (value: unknown, context: RuleFieldContext): Promise => { for (const rule of rules.value) { diff --git a/packages/validation/src/rules/confirm.ts b/packages/validation/src/rules/confirm.ts index ed67b974..34c4dd7f 100644 --- a/packages/validation/src/rules/confirm.ts +++ b/packages/validation/src/rules/confirm.ts @@ -1,5 +1,17 @@ import { defineRule } from '../defineRule.js' +/** + * Creates a validation rule that checks if a field's value matches a comparison value. + * Typically used for password confirmation fields. + * @param comparingValue - The value to compare against (e.g., the original password) + * @returns A validation rule function + * @example + * // In a password confirmation field: + * const passwordValue = 'myPassword123' + * const confirmRule = confirm(passwordValue) + * confirmRule('myPassword123', { name: 'Passwort bestätigen' }) // true + * confirmRule('different', { name: 'Passwort bestätigen' }) // 'Das Feld Passwort bestätigen ist nicht gleich' + */ export function confirm(comparingValue?: string) { return defineRule((value, context) => { const valid = value === comparingValue diff --git a/packages/validation/src/rules/emailValid.ts b/packages/validation/src/rules/emailValid.ts index 88d33a93..5dd38fc0 100644 --- a/packages/validation/src/rules/emailValid.ts +++ b/packages/validation/src/rules/emailValid.ts @@ -1,5 +1,15 @@ const emailRegex = new RegExp('^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$') const errorMessage = 'Die Email hat nicht das korrekte Format' + +/** + * Validates that a value is a properly formatted email address. + * Checks for valid email format and ensures no spaces are present. + * @param value - The value to validate + * @returns True if the value is a valid email, or an error message in German if invalid + * @example + * emailValid('user@example.com') // true + * emailValid('invalid email') // 'Die Email hat nicht das korrekte Format' + */ export function emailValid(value: unknown) { if (typeof value !== 'string') return errorMessage const emailValid = emailRegex.test(value) && !value.includes(' ') diff --git a/packages/validation/src/rules/required.ts b/packages/validation/src/rules/required.ts index 93b1caf4..e6435140 100644 --- a/packages/validation/src/rules/required.ts +++ b/packages/validation/src/rules/required.ts @@ -1,6 +1,22 @@ import { defineRule } from '../defineRule.js' +/** + * Parameters for the required validation rule. + * Can be a boolean or an object with allowFalse option. + */ export type RequiredRulesParams = boolean | { allowFalse: boolean } + +/** + * Creates a validation rule that checks if a field has a value. + * By default, empty strings, null, undefined, and false are considered invalid. + * @param params - If false, the field is not required. If true or an object, the field is required. + * The object form accepts an `allowFalse` property to allow boolean false as a valid value. + * @returns A validation rule function + * @example + * required(true) // Field is required, false values are invalid + * required({ allowFalse: true }) // Field is required, but false is allowed + * required(false) // Field is not required + */ export function required(params: RequiredRulesParams) { return defineRule((value, context) => { const allowFalse = typeof params !== 'boolean' && params.allowFalse From 752e1fbf8bdb36781ab38eb583ce7bf2efde3b7d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:47:34 +0000 Subject: [PATCH 16/21] Add JSDoc to apps/frontend/src/helpers utilities Co-authored-by: danielswiatek <9080990+danielswiatek@users.noreply.github.com> --- apps/frontend/src/helpers/cn.ts | 11 +++++++ apps/frontend/src/helpers/debounce.ts | 35 +++++++++++++++++++++++ apps/frontend/src/helpers/handleUpload.ts | 32 +++++++++++++++++++-- apps/frontend/src/helpers/params.ts | 8 ++++++ 4 files changed, 84 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/helpers/cn.ts b/apps/frontend/src/helpers/cn.ts index 7f36179b..39e2bc51 100644 --- a/apps/frontend/src/helpers/cn.ts +++ b/apps/frontend/src/helpers/cn.ts @@ -1,6 +1,17 @@ import { type ClassValue, clsx } from 'clsx' import { twMerge } from 'tailwind-merge' +/** + * Merges CSS class names intelligently using clsx and tailwind-merge. + * This function combines class names and resolves Tailwind CSS conflicts, + * ensuring the last class wins when there are duplicates. + * @param inputs - Class names, objects, or arrays of classes to merge + * @returns A merged string of class names + * @example + * cn('px-4 py-2', 'bg-blue-500') // 'px-4 py-2 bg-blue-500' + * cn('px-4', 'px-8') // 'px-8' (last one wins) + * cn({ 'text-red-500': true, 'hidden': false }) // 'text-red-500' + */ export default function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } diff --git a/apps/frontend/src/helpers/debounce.ts b/apps/frontend/src/helpers/debounce.ts index 1401dcce..60cbe0f9 100644 --- a/apps/frontend/src/helpers/debounce.ts +++ b/apps/frontend/src/helpers/debounce.ts @@ -1,12 +1,25 @@ /* eslint-disable @typescript-eslint/no-unused-expressions */ +/** + * Options for configuring the debounce behavior. + * @template Result - The return type of the debounced function + */ export type Options = { + /** If true, the function is invoked immediately on the first call */ isImmediate?: boolean + /** Maximum time in milliseconds the function can be delayed before being invoked */ maxWait?: number + /** Optional callback to execute when the function is invoked */ callback?: (data: Result) => void } +/** + * A debounced function with a cancel method. + * @template Args - The argument types of the function + * @template F - The function type + */ export interface DebouncedFunction any> { (this: ThisParameterType, ...args: Args & Parameters): Promise> + /** Cancels any pending invocations */ cancel: (reason?: any) => void } @@ -15,6 +28,28 @@ interface DebouncedPromise { reject: (reason?: any) => void } +/** + * Creates a debounced version of a function that delays its execution until after + * a specified wait time has elapsed since the last time it was invoked. + * + * This is useful for performance optimization, such as limiting the rate of expensive + * operations like API calls or DOM updates in response to user input. + * @template Args - The argument types of the function + * @template F - The function type + * @param func - The function to debounce + * @param waitMilliseconds - The number of milliseconds to delay (default: 50) + * @param options - Additional options for debounce behavior + * @returns A debounced version of the function that returns a Promise + * @example + * const debouncedSearch = debounce((query: string) => { + * return apiClient.search(query) + * }, 300) + * + * // This will only execute once after 300ms of no calls + * debouncedSearch('test') + * debouncedSearch('test2') + * debouncedSearch('test3') + */ export function debounce any>( func: F, waitMilliseconds = 50, diff --git a/apps/frontend/src/helpers/handleUpload.ts b/apps/frontend/src/helpers/handleUpload.ts index c9e71354..0b66cbe9 100644 --- a/apps/frontend/src/helpers/handleUpload.ts +++ b/apps/frontend/src/helpers/handleUpload.ts @@ -15,8 +15,18 @@ export function handleUpload(file: File): Promise<{ id: string; mimetype: string export function handleUpload(file: File[]): Promise<{ id: string; mimetype: string }[]> /** - * Upload file - * @param file + * Uploads one or multiple files to the configured storage provider (Azure or Local). + * Supports overloading to handle both single files and arrays of files. + * @param file - A single File object or an array of File objects to upload + * @returns A Promise resolving to the uploaded file metadata (id and mimetype) + * @example + * // Upload a single file + * const result = await handleUpload(fileInput.files[0]) + * console.log(result.id) // File ID in database + * + * // Upload multiple files + * const results = await handleUpload([...fileInput.files]) + * results.forEach(file => console.log(file.id)) */ export async function handleUpload(file: File | File[]): Promise { if (Array.isArray(file)) { @@ -33,6 +43,15 @@ export async function handleUpload(file: File | File[]): Promise @@ -41,6 +60,15 @@ export async function handlePublicPhotoUpload( return await uploadFile(file, dbFile) } +/** + * Internal helper function that uploads a file to the appropriate storage provider. + * Supports both LOCAL (via HTTP) and AZURE (via Azure Blob Storage) providers. + * @param file - The file to upload + * @param dbFile - Database file record with upload configuration + * @returns The uploaded file metadata + * @throws Error if the upload fails or if the provider is not supported + * @internal + */ async function uploadFile(file: File, dbFile: dbFile): Promise { const formData = new FormData() formData.append('file', file) diff --git a/apps/frontend/src/helpers/params.ts b/apps/frontend/src/helpers/params.ts index f21e5aa6..1bc9ad61 100644 --- a/apps/frontend/src/helpers/params.ts +++ b/apps/frontend/src/helpers/params.ts @@ -1,5 +1,13 @@ import { useRoute } from 'vue-router' +/** + * Extracts and parses an integer parameter from the current route. + * @param name - The name of the route parameter + * @returns The parsed integer value, or NaN if the parameter is an array or cannot be parsed + * @example + * // For route /users/:id + * const userId = paramInt('id') // Returns the parsed integer ID + */ export function paramInt(name: string) { const route = useRoute() const id = route.params[name] From df10198e33b2507e24b2b7a3c4b534dec0fd40fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:51:51 +0000 Subject: [PATCH 17/21] Initial plan From d7830e05cb71d2aad0601ad4acfd807cba6ece53 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:55:26 +0000 Subject: [PATCH 18/21] Fix: Refresh event details data after saving in Marketing tab Co-authored-by: danielswiatek <9080990+danielswiatek@users.noreply.github.com> --- .../FormUnterveranstaltungLandingSettings.vue | 31 ++++++++++++++++++- .../UnterveranstaltungDetail.vue | 6 +++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/components/forms/unterveranstaltung/FormUnterveranstaltungLandingSettings.vue b/apps/frontend/src/components/forms/unterveranstaltung/FormUnterveranstaltungLandingSettings.vue index f2d0c47b..7a72fbc9 100644 --- a/apps/frontend/src/components/forms/unterveranstaltung/FormUnterveranstaltungLandingSettings.vue +++ b/apps/frontend/src/components/forms/unterveranstaltung/FormUnterveranstaltungLandingSettings.vue @@ -1,7 +1,7 @@