feat: configureerbare itemnamen per vragenlijst

Voeg item_label en item_label_plural toe aan questionnaires (migratie),
beheerformulier en dynamische teksten in UI en API-fouten. Standaard
blijft Activiteit/Activiteiten.

Negeer tsbuildinfo en geëmitteerde vite.config.js/.d.ts in .gitignore.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-10 15:29:44 +02:00
parent 6871126788
commit 93aedf319b
9 changed files with 234 additions and 42 deletions

View File

@@ -5,7 +5,14 @@ import multer, { FileFilterCallback } from 'multer';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { userOps, questionnaireOps, activityOps, participantOps } from '../database.js';
import {
userOps,
questionnaireOps,
activityOps,
participantOps,
resolveItemLabels,
DEFAULT_ITEM_LABEL,
} from '../database.js';
import { requireAuth } from '../middleware/auth.js';
const __filename = fileURLToPath(import.meta.url);
@@ -74,9 +81,17 @@ function isValidSlug(slug: string): boolean {
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug);
}
function parseItemLabelsFromBody(itemLabel: unknown, itemLabelPlural: unknown): { singular: string; plural: string } {
return resolveItemLabels({
item_label: typeof itemLabel === 'string' ? itemLabel : null,
item_label_plural: typeof itemLabelPlural === 'string' ? itemLabelPlural : null,
});
}
// Create questionnaire
router.post('/questionnaires', (req: Request, res: Response) => {
const { title, description, slug, ogImage, isPrivate } = req.body;
const { title, description, slug, ogImage, isPrivate, itemLabel, itemLabelPlural } = req.body;
const { singular, plural } = parseItemLabelsFromBody(itemLabel, itemLabelPlural);
if (!title?.trim()) {
res.status(400).json({ error: 'Titel is verplicht' });
@@ -106,7 +121,17 @@ router.post('/questionnaires', (req: Request, res: Response) => {
}
const uuid = uuidv4();
const id = questionnaireOps.create(uuid, cleanSlug, title.trim(), description?.trim() || null, ogImage?.trim() || null, !!isPrivate, req.session.user!.id);
const id = questionnaireOps.create(
uuid,
cleanSlug,
title.trim(),
description?.trim() || null,
ogImage?.trim() || null,
!!isPrivate,
req.session.user!.id,
singular,
plural
);
const questionnaire = questionnaireOps.findById(id);
res.json({ success: true, questionnaire });
@@ -127,7 +152,8 @@ router.get('/questionnaires/:id', (req: Request, res: Response) => {
// Update questionnaire
router.put('/questionnaires/:id', (req: Request, res: Response) => {
const { title, description, slug, ogImage, isPrivate } = req.body;
const { title, description, slug, ogImage, isPrivate, itemLabel, itemLabelPlural } = req.body;
const { singular, plural } = parseItemLabelsFromBody(itemLabel, itemLabelPlural);
const questionnaire = questionnaireOps.findById(parseInt(req.params.id));
if (!questionnaire) {
@@ -162,7 +188,16 @@ router.put('/questionnaires/:id', (req: Request, res: Response) => {
return;
}
questionnaireOps.update(questionnaire.id, cleanSlug, title.trim(), description?.trim() || null, ogImage?.trim() || null, !!isPrivate);
questionnaireOps.update(
questionnaire.id,
cleanSlug,
title.trim(),
description?.trim() || null,
ogImage?.trim() || null,
!!isPrivate,
singular,
plural
);
res.json({ success: true });
});
@@ -267,12 +302,15 @@ router.put('/activities/:id', (req: Request, res: Response) => {
const activity = activityOps.findById(parseInt(req.params.id));
if (!activity) {
res.status(404).json({ error: 'Activiteit niet gevonden' });
res.status(404).json({ error: `${DEFAULT_ITEM_LABEL} niet gevonden` });
return;
}
const q = questionnaireOps.findById(activity.questionnaire_id);
const itemSingular = q ? resolveItemLabels(q).singular : DEFAULT_ITEM_LABEL;
if (!name?.trim()) {
res.status(400).json({ error: 'Naam activiteit is verplicht' });
res.status(400).json({ error: `Naam ${itemSingular.toLowerCase()} is verplicht` });
return;
}