Initial commit: Activiteiten Inventaris applicatie
This commit is contained in:
283
server/routes/admin.ts
Normal file
283
server/routes/admin.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import crypto from 'crypto';
|
||||
import { userOps, questionnaireOps, activityOps, participantOps } from '../database.js';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Apply auth middleware to all admin routes
|
||||
router.use(requireAuth);
|
||||
|
||||
// Get all questionnaires
|
||||
router.get('/questionnaires', (req: Request, res: Response) => {
|
||||
const questionnaires = questionnaireOps.getAll();
|
||||
res.json(questionnaires);
|
||||
});
|
||||
|
||||
// Validate slug format
|
||||
function isValidSlug(slug: string): boolean {
|
||||
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug);
|
||||
}
|
||||
|
||||
// Create questionnaire
|
||||
router.post('/questionnaires', (req: Request, res: Response) => {
|
||||
const { title, description, slug, isPrivate } = req.body;
|
||||
|
||||
if (!title?.trim()) {
|
||||
res.status(400).json({ error: 'Titel is verplicht' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!slug?.trim()) {
|
||||
res.status(400).json({ error: 'Slug is verplicht' });
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanSlug = slug.trim().toLowerCase();
|
||||
|
||||
if (!isValidSlug(cleanSlug)) {
|
||||
res.status(400).json({ error: 'Slug mag alleen kleine letters, cijfers en koppeltekens bevatten' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (cleanSlug.length < 3 || cleanSlug.length > 50) {
|
||||
res.status(400).json({ error: 'Slug moet tussen 3 en 50 tekens zijn' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!questionnaireOps.isSlugAvailable(cleanSlug)) {
|
||||
res.status(400).json({ error: 'Deze slug is al in gebruik' });
|
||||
return;
|
||||
}
|
||||
|
||||
const uuid = uuidv4();
|
||||
const id = questionnaireOps.create(uuid, cleanSlug, title.trim(), description?.trim() || null, !!isPrivate, req.session.user!.id);
|
||||
const questionnaire = questionnaireOps.findById(id);
|
||||
|
||||
res.json({ success: true, questionnaire });
|
||||
});
|
||||
|
||||
// Get questionnaire by ID
|
||||
router.get('/questionnaires/:id', (req: Request, res: Response) => {
|
||||
const questionnaire = questionnaireOps.findById(parseInt(req.params.id));
|
||||
|
||||
if (!questionnaire) {
|
||||
res.status(404).json({ error: 'Vragenlijst niet gevonden' });
|
||||
return;
|
||||
}
|
||||
|
||||
const activities = activityOps.getByQuestionnaire(questionnaire.id);
|
||||
res.json({ questionnaire, activities });
|
||||
});
|
||||
|
||||
// Update questionnaire
|
||||
router.put('/questionnaires/:id', (req: Request, res: Response) => {
|
||||
const { title, description, slug, isPrivate } = req.body;
|
||||
const questionnaire = questionnaireOps.findById(parseInt(req.params.id));
|
||||
|
||||
if (!questionnaire) {
|
||||
res.status(404).json({ error: 'Vragenlijst niet gevonden' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!title?.trim()) {
|
||||
res.status(400).json({ error: 'Titel is verplicht' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!slug?.trim()) {
|
||||
res.status(400).json({ error: 'Slug is verplicht' });
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanSlug = slug.trim().toLowerCase();
|
||||
|
||||
if (!isValidSlug(cleanSlug)) {
|
||||
res.status(400).json({ error: 'Slug mag alleen kleine letters, cijfers en koppeltekens bevatten' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (cleanSlug.length < 3 || cleanSlug.length > 50) {
|
||||
res.status(400).json({ error: 'Slug moet tussen 3 en 50 tekens zijn' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!questionnaireOps.isSlugAvailable(cleanSlug, questionnaire.id)) {
|
||||
res.status(400).json({ error: 'Deze slug is al in gebruik' });
|
||||
return;
|
||||
}
|
||||
|
||||
questionnaireOps.update(questionnaire.id, cleanSlug, title.trim(), description?.trim() || null, !!isPrivate);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// Check slug availability
|
||||
router.get('/questionnaires/check-slug/:slug', (req: Request, res: Response) => {
|
||||
const { slug } = req.params;
|
||||
const { excludeId } = req.query;
|
||||
|
||||
const cleanSlug = slug.trim().toLowerCase();
|
||||
const available = questionnaireOps.isSlugAvailable(cleanSlug, excludeId ? parseInt(excludeId as string) : undefined);
|
||||
|
||||
res.json({ available });
|
||||
});
|
||||
|
||||
// Get participants for a questionnaire
|
||||
router.get('/questionnaires/:id/participants', (req: Request, res: Response) => {
|
||||
const questionnaire = questionnaireOps.findById(parseInt(req.params.id));
|
||||
|
||||
if (!questionnaire) {
|
||||
res.status(404).json({ error: 'Vragenlijst niet gevonden' });
|
||||
return;
|
||||
}
|
||||
|
||||
const participants = participantOps.getByQuestionnaire(questionnaire.id);
|
||||
res.json(participants);
|
||||
});
|
||||
|
||||
// Generate unique token
|
||||
function generateToken(): string {
|
||||
return crypto.randomBytes(16).toString('hex');
|
||||
}
|
||||
|
||||
// Add participant to questionnaire
|
||||
router.post('/questionnaires/:id/participants', (req: Request, res: Response) => {
|
||||
const { name } = req.body;
|
||||
const questionnaire = questionnaireOps.findById(parseInt(req.params.id));
|
||||
|
||||
if (!questionnaire) {
|
||||
res.status(404).json({ error: 'Vragenlijst niet gevonden' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!name?.trim()) {
|
||||
res.status(400).json({ error: 'Naam is verplicht' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate unique token
|
||||
let token = generateToken();
|
||||
while (!participantOps.isTokenAvailable(token)) {
|
||||
token = generateToken();
|
||||
}
|
||||
|
||||
const id = participantOps.create(questionnaire.id, name.trim(), token);
|
||||
const participant = participantOps.findById(id);
|
||||
|
||||
res.json({ success: true, participant });
|
||||
});
|
||||
|
||||
// Delete participant
|
||||
router.delete('/participants/:id', (req: Request, res: Response) => {
|
||||
participantOps.delete(parseInt(req.params.id));
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// Delete questionnaire
|
||||
router.delete('/questionnaires/:id', (req: Request, res: Response) => {
|
||||
questionnaireOps.delete(parseInt(req.params.id));
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// Update activity
|
||||
router.put('/activities/:id', (req: Request, res: Response) => {
|
||||
const { name, description } = req.body;
|
||||
const activity = activityOps.findById(parseInt(req.params.id));
|
||||
|
||||
if (!activity) {
|
||||
res.status(404).json({ error: 'Activiteit niet gevonden' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!name?.trim()) {
|
||||
res.status(400).json({ error: 'Naam activiteit is verplicht' });
|
||||
return;
|
||||
}
|
||||
|
||||
activityOps.update(activity.id, name.trim(), description?.trim() || null);
|
||||
const updatedActivity = activityOps.findById(activity.id);
|
||||
res.json({ success: true, activity: updatedActivity });
|
||||
});
|
||||
|
||||
// Delete activity
|
||||
router.delete('/activities/:id', (req: Request, res: Response) => {
|
||||
activityOps.delete(parseInt(req.params.id));
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// Get all users
|
||||
router.get('/users', (req: Request, res: Response) => {
|
||||
const users = userOps.getAll();
|
||||
res.json(users);
|
||||
});
|
||||
|
||||
// Create user
|
||||
router.post('/users', (req: Request, res: Response) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
res.status(400).json({ error: 'Gebruikersnaam en wachtwoord zijn verplicht' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
res.status(400).json({ error: 'Wachtwoord moet minimaal 6 tekens zijn' });
|
||||
return;
|
||||
}
|
||||
|
||||
const existingUser = userOps.findByUsername(username);
|
||||
if (existingUser) {
|
||||
res.status(400).json({ error: 'Gebruikersnaam bestaat al' });
|
||||
return;
|
||||
}
|
||||
|
||||
const id = userOps.create(username, password);
|
||||
const user = userOps.findById(id);
|
||||
res.json({ success: true, user });
|
||||
});
|
||||
|
||||
// Delete user
|
||||
router.delete('/users/:id', (req: Request, res: Response) => {
|
||||
const userId = parseInt(req.params.id);
|
||||
|
||||
if (userId === req.session.user!.id) {
|
||||
res.status(400).json({ error: 'Je kunt jezelf niet verwijderen' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (userOps.count() <= 1) {
|
||||
res.status(400).json({ error: 'Je kunt de laatste beheerder niet verwijderen' });
|
||||
return;
|
||||
}
|
||||
|
||||
userOps.delete(userId);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// Change password
|
||||
router.post('/change-password', (req: Request, res: Response) => {
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
|
||||
const user = userOps.findByUsername(req.session.user!.username);
|
||||
if (!user) {
|
||||
res.status(404).json({ error: 'Gebruiker niet gevonden' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userOps.verifyPassword(user, currentPassword)) {
|
||||
res.status(400).json({ error: 'Huidig wachtwoord is onjuist' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
res.status(400).json({ error: 'Wachtwoord moet minimaal 6 tekens zijn' });
|
||||
return;
|
||||
}
|
||||
|
||||
userOps.updatePassword(req.session.user!.id, newPassword);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
50
server/routes/auth.ts
Normal file
50
server/routes/auth.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { userOps } from '../database.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Check auth status
|
||||
router.get('/status', (req: Request, res: Response) => {
|
||||
if (req.session.user) {
|
||||
res.json({ authenticated: true, user: req.session.user });
|
||||
} else {
|
||||
res.json({ authenticated: false });
|
||||
}
|
||||
});
|
||||
|
||||
// Login
|
||||
router.post('/login', (req: Request, res: Response) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
res.status(400).json({ error: 'Gebruikersnaam en wachtwoord zijn verplicht' });
|
||||
return;
|
||||
}
|
||||
|
||||
const user = userOps.findByUsername(username);
|
||||
|
||||
if (!user || !userOps.verifyPassword(user, password)) {
|
||||
res.status(401).json({ error: 'Ongeldige gebruikersnaam of wachtwoord' });
|
||||
return;
|
||||
}
|
||||
|
||||
req.session.user = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
};
|
||||
|
||||
res.json({ success: true, user: req.session.user });
|
||||
});
|
||||
|
||||
// Logout
|
||||
router.post('/logout', (req: Request, res: Response) => {
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
console.error('Session destroy error:', err);
|
||||
}
|
||||
res.json({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
366
server/routes/questionnaire.ts
Normal file
366
server/routes/questionnaire.ts
Normal file
@@ -0,0 +1,366 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { questionnaireOps, activityOps, voteOps, commentOps, participantOps, Comment, Questionnaire } from '../database.js';
|
||||
|
||||
// Helper to check if user can participate (write access)
|
||||
function canParticipate(questionnaire: Questionnaire, req: Request): { canWrite: boolean; participantName: string | null } {
|
||||
// Check for participant token first
|
||||
const token = req.cookies.participantToken;
|
||||
if (token) {
|
||||
const participant = participantOps.findByToken(token);
|
||||
if (participant && participant.questionnaire_id === questionnaire.id) {
|
||||
return { canWrite: true, participantName: participant.name };
|
||||
}
|
||||
}
|
||||
|
||||
// For public questionnaires, use visitor name
|
||||
if (!questionnaire.is_private) {
|
||||
const visitorName = req.cookies.visitorName;
|
||||
return { canWrite: !!visitorName, participantName: visitorName || null };
|
||||
}
|
||||
|
||||
// Private questionnaire without valid token
|
||||
return { canWrite: false, participantName: null };
|
||||
}
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Access questionnaire via participant token
|
||||
router.get('/token/:token', (req: Request, res: Response) => {
|
||||
const participant = participantOps.findByToken(req.params.token);
|
||||
|
||||
if (!participant) {
|
||||
res.status(404).json({ error: 'Ongeldige toegangslink' });
|
||||
return;
|
||||
}
|
||||
|
||||
const questionnaire = questionnaireOps.findById(participant.questionnaire_id);
|
||||
|
||||
if (!questionnaire) {
|
||||
res.status(404).json({ error: 'Vragenlijst niet gevonden' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Set participant token cookie
|
||||
res.cookie('participantToken', participant.token, {
|
||||
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
});
|
||||
|
||||
const activities = activityOps.getByQuestionnaire(questionnaire.id);
|
||||
|
||||
let userVotes: Record<number, number> = {};
|
||||
const votes = voteOps.getByQuestionnaireAndVoter(questionnaire.id, participant.name);
|
||||
votes.forEach(v => {
|
||||
userVotes[v.activity_id] = v.vote_type;
|
||||
});
|
||||
|
||||
res.json({
|
||||
questionnaire,
|
||||
activities,
|
||||
visitorName: participant.name,
|
||||
userVotes,
|
||||
isParticipant: true,
|
||||
canWrite: true,
|
||||
});
|
||||
});
|
||||
|
||||
// Get questionnaire by slug (public - may be read-only for private questionnaires)
|
||||
router.get('/:slug', (req: Request, res: Response) => {
|
||||
const questionnaire = questionnaireOps.findBySlug(req.params.slug);
|
||||
|
||||
if (!questionnaire) {
|
||||
res.status(404).json({ error: 'Vragenlijst niet gevonden' });
|
||||
return;
|
||||
}
|
||||
|
||||
const activities = activityOps.getByQuestionnaire(questionnaire.id);
|
||||
const { canWrite, participantName } = canParticipate(questionnaire, req);
|
||||
|
||||
let userVotes: Record<number, number> = {};
|
||||
if (participantName) {
|
||||
const votes = voteOps.getByQuestionnaireAndVoter(questionnaire.id, participantName);
|
||||
votes.forEach(v => {
|
||||
userVotes[v.activity_id] = v.vote_type;
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
questionnaire,
|
||||
activities,
|
||||
visitorName: participantName || '',
|
||||
userVotes,
|
||||
isPrivate: !!questionnaire.is_private,
|
||||
canWrite,
|
||||
});
|
||||
});
|
||||
|
||||
// Set visitor name (only for public questionnaires)
|
||||
router.post('/:slug/set-name', (req: Request, res: Response) => {
|
||||
const { name } = req.body;
|
||||
const questionnaire = questionnaireOps.findBySlug(req.params.slug);
|
||||
|
||||
if (!questionnaire) {
|
||||
res.status(404).json({ error: 'Vragenlijst niet gevonden' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (questionnaire.is_private) {
|
||||
res.status(403).json({ error: 'Voor deze vragenlijst is een uitnodigingslink nodig' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!name?.trim()) {
|
||||
res.status(400).json({ error: 'Naam is verplicht' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.cookie('visitorName', name.trim(), {
|
||||
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
});
|
||||
|
||||
res.json({ success: true, name: name.trim() });
|
||||
});
|
||||
|
||||
// Clear visitor name
|
||||
router.post('/:slug/clear-name', (req: Request, res: Response) => {
|
||||
res.clearCookie('visitorName');
|
||||
res.clearCookie('participantToken');
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// Add activity
|
||||
router.post('/:slug/activities', (req: Request, res: Response) => {
|
||||
const { name, description } = req.body;
|
||||
const questionnaire = questionnaireOps.findBySlug(req.params.slug);
|
||||
|
||||
if (!questionnaire) {
|
||||
res.status(404).json({ error: 'Vragenlijst niet gevonden' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { canWrite, participantName } = canParticipate(questionnaire, req);
|
||||
|
||||
if (!canWrite || !participantName) {
|
||||
if (questionnaire.is_private) {
|
||||
res.status(403).json({ error: 'Je hebt een uitnodigingslink nodig om deel te nemen' });
|
||||
} else {
|
||||
res.status(401).json({ error: 'Voer eerst je naam in' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!name?.trim()) {
|
||||
res.status(400).json({ error: 'Naam activiteit is verplicht' });
|
||||
return;
|
||||
}
|
||||
|
||||
const activityDescription = description?.trim() || null;
|
||||
const activityId = activityOps.create(questionnaire.id, name.trim(), participantName, activityDescription);
|
||||
const activities = activityOps.getByQuestionnaire(questionnaire.id);
|
||||
const activity = activities.find(a => a.id === activityId);
|
||||
|
||||
res.json({ success: true, activity });
|
||||
});
|
||||
|
||||
// Update activity (only by the user who added it)
|
||||
router.put('/:slug/activities/:activityId', (req: Request, res: Response) => {
|
||||
const { name, description } = req.body;
|
||||
const questionnaire = questionnaireOps.findBySlug(req.params.slug);
|
||||
|
||||
if (!questionnaire) {
|
||||
res.status(404).json({ error: 'Vragenlijst niet gevonden' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { canWrite, participantName } = canParticipate(questionnaire, req);
|
||||
|
||||
if (!canWrite || !participantName) {
|
||||
if (questionnaire.is_private) {
|
||||
res.status(403).json({ error: 'Je hebt een uitnodigingslink nodig om te bewerken' });
|
||||
} else {
|
||||
res.status(401).json({ error: 'Voer eerst je naam in' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const activity = activityOps.findById(parseInt(req.params.activityId));
|
||||
if (!activity || activity.questionnaire_id !== questionnaire.id) {
|
||||
res.status(404).json({ error: 'Activiteit niet gevonden' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the user is the one who added the activity
|
||||
if (activity.added_by !== participantName) {
|
||||
res.status(403).json({ error: 'Je kunt alleen je eigen activiteiten bewerken' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!name?.trim()) {
|
||||
res.status(400).json({ error: 'Naam activiteit is verplicht' });
|
||||
return;
|
||||
}
|
||||
|
||||
activityOps.update(activity.id, name.trim(), description?.trim() || null);
|
||||
const activities = activityOps.getByQuestionnaire(questionnaire.id);
|
||||
const updatedActivity = activities.find(a => a.id === activity.id);
|
||||
|
||||
res.json({ success: true, activity: updatedActivity });
|
||||
});
|
||||
|
||||
// Vote on activity
|
||||
router.post('/:slug/activities/:activityId/vote', (req: Request, res: Response) => {
|
||||
const { voteType } = req.body;
|
||||
const questionnaire = questionnaireOps.findBySlug(req.params.slug);
|
||||
|
||||
if (!questionnaire) {
|
||||
res.status(404).json({ error: 'Vragenlijst niet gevonden' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { canWrite, participantName } = canParticipate(questionnaire, req);
|
||||
|
||||
if (!canWrite || !participantName) {
|
||||
if (questionnaire.is_private) {
|
||||
res.status(403).json({ error: 'Je hebt een uitnodigingslink nodig om te stemmen' });
|
||||
} else {
|
||||
res.status(401).json({ error: 'Voer eerst je naam in' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const activity = activityOps.findById(parseInt(req.params.activityId));
|
||||
if (!activity || activity.questionnaire_id !== questionnaire.id) {
|
||||
res.status(404).json({ error: 'Activiteit niet gevonden' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (voteType !== 1 && voteType !== -1) {
|
||||
res.status(400).json({ error: 'Ongeldig stemtype' });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = voteOps.upsert(activity.id, participantName, voteType);
|
||||
const activities = activityOps.getByQuestionnaire(questionnaire.id);
|
||||
const updatedActivity = activities.find(a => a.id === activity.id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
action: result.action,
|
||||
currentVote: result.voteType,
|
||||
activity: updatedActivity,
|
||||
});
|
||||
});
|
||||
|
||||
// Get activities
|
||||
router.get('/:slug/activities', (req: Request, res: Response) => {
|
||||
const questionnaire = questionnaireOps.findBySlug(req.params.slug);
|
||||
|
||||
if (!questionnaire) {
|
||||
res.status(404).json({ error: 'Vragenlijst niet gevonden' });
|
||||
return;
|
||||
}
|
||||
|
||||
const activities = activityOps.getByQuestionnaire(questionnaire.id);
|
||||
const visitorName = req.cookies.visitorName || '';
|
||||
|
||||
let userVotes: Record<number, number> = {};
|
||||
if (visitorName) {
|
||||
const votes = voteOps.getByQuestionnaireAndVoter(questionnaire.id, visitorName);
|
||||
votes.forEach(v => {
|
||||
userVotes[v.activity_id] = v.vote_type;
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ activities, userVotes });
|
||||
});
|
||||
|
||||
// Get comments for an activity
|
||||
router.get('/:slug/activities/:activityId/comments', (req: Request, res: Response) => {
|
||||
const questionnaire = questionnaireOps.findBySlug(req.params.slug);
|
||||
|
||||
if (!questionnaire) {
|
||||
res.status(404).json({ error: 'Vragenlijst niet gevonden' });
|
||||
return;
|
||||
}
|
||||
|
||||
const activity = activityOps.findById(parseInt(req.params.activityId));
|
||||
if (!activity || activity.questionnaire_id !== questionnaire.id) {
|
||||
res.status(404).json({ error: 'Activiteit niet gevonden' });
|
||||
return;
|
||||
}
|
||||
|
||||
const comments = commentOps.getByActivity(activity.id);
|
||||
|
||||
// Build nested comment tree
|
||||
const commentMap: Record<number, Comment> = {};
|
||||
const rootComments: Comment[] = [];
|
||||
|
||||
comments.forEach(comment => {
|
||||
comment.replies = [];
|
||||
commentMap[comment.id] = comment;
|
||||
});
|
||||
|
||||
comments.forEach(comment => {
|
||||
if (comment.parent_id) {
|
||||
if (commentMap[comment.parent_id]) {
|
||||
commentMap[comment.parent_id].replies!.push(comment);
|
||||
}
|
||||
} else {
|
||||
rootComments.push(comment);
|
||||
}
|
||||
});
|
||||
|
||||
res.json({ comments: rootComments, activity });
|
||||
});
|
||||
|
||||
// Add a comment
|
||||
router.post('/:slug/activities/:activityId/comments', (req: Request, res: Response) => {
|
||||
const { content, parentId } = req.body;
|
||||
const questionnaire = questionnaireOps.findBySlug(req.params.slug);
|
||||
|
||||
if (!questionnaire) {
|
||||
res.status(404).json({ error: 'Vragenlijst niet gevonden' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { canWrite, participantName } = canParticipate(questionnaire, req);
|
||||
|
||||
if (!canWrite || !participantName) {
|
||||
if (questionnaire.is_private) {
|
||||
res.status(403).json({ error: 'Je hebt een uitnodigingslink nodig om te reageren' });
|
||||
} else {
|
||||
res.status(401).json({ error: 'Voer eerst je naam in' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const activity = activityOps.findById(parseInt(req.params.activityId));
|
||||
if (!activity || activity.questionnaire_id !== questionnaire.id) {
|
||||
res.status(404).json({ error: 'Activiteit niet gevonden' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content?.trim()) {
|
||||
res.status(400).json({ error: 'Reactie inhoud is verplicht' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (parentId) {
|
||||
const parentComment = commentOps.getById(parentId);
|
||||
if (!parentComment || parentComment.activity_id !== activity.id) {
|
||||
res.status(400).json({ error: 'Ongeldige reactie om op te reageren' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const commentId = commentOps.create(activity.id, participantName, content.trim(), parentId || null);
|
||||
const comment = commentOps.getById(commentId);
|
||||
|
||||
res.json({ success: true, comment: { ...comment, replies: [] } });
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user