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/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/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..f2603a0b --- /dev/null +++ b/apps/api/src/routes/exports/unterschriftenliste.sheet.ts @@ -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) +} diff --git a/apps/api/src/services/veranstaltung/veranstaltung.list.ts b/apps/api/src/services/veranstaltung/veranstaltung.list.ts index fc63da28..710f6765 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, @@ -33,6 +38,8 @@ export const unterveranstaltungSelect: Prisma.UnterveranstaltungSelect = { teilnahmegebuehr: true, meldebeginn: true, meldeschluss: true, + beschreibung: true, + bedingungen: true, gliederungId: true, _count: { select: { 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') 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/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/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() { 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', diff --git a/apps/frontend/src/components/forms/unterveranstaltung/FormUnterveranstaltungLandingSettings.vue b/apps/frontend/src/components/forms/unterveranstaltung/FormUnterveranstaltungLandingSettings.vue index f2d0c47b..7346bb88 100644 --- a/apps/frontend/src/components/forms/unterveranstaltung/FormUnterveranstaltungLandingSettings.vue +++ b/apps/frontend/src/components/forms/unterveranstaltung/FormUnterveranstaltungLandingSettings.vue @@ -1,7 +1,7 @@