feat: Implement member inbox with read status and question functionality
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 53s

This commit is contained in:
Torsten Schulz (local)
2026-07-24 09:17:57 +02:00
parent 63a5db1196
commit e2e8ba0d54
9 changed files with 211 additions and 17 deletions

View File

@@ -9,6 +9,7 @@ import CalendarEvent from '../models/CalendarEvent.js';
import TrainingGroup from '../models/TrainingGroup.js';
import ClubCommunicationRecipient from '../models/ClubCommunicationRecipient.js';
import ClubCommunicationThread from '../models/ClubCommunicationThread.js';
import ClubCommunicationMessage from '../models/ClubCommunicationMessage.js';
import ClubSepaMandate from '../models/ClubSepaMandate.js';
import MemberProfileChangeRequest from '../models/MemberProfileChangeRequest.js';
import MemberEventResponse from '../models/MemberEventResponse.js';
@@ -534,6 +535,90 @@ const reviewProfileChangeRequest = async (req, res) => {
}
};
const getMemberInbox = async (req, res) => {
try {
const clubId = Number(req.params.clubId);
const userId = Number(req.user?.id);
const access = await getApprovedLinkedMember(clubId, userId);
if (access.error) return res.status(access.status).json({ error: access.error });
const recipients = await ClubCommunicationRecipient.findAll({
where: { clubId, memberId: access.member.id, deliveryStatus: 'sent' },
include: [{
model: ClubCommunicationThread,
as: 'thread',
required: true,
where: { status: 'sent' },
attributes: ['id', 'subject', 'threadType', 'replyTo', 'sentAt'],
include: [{ model: ClubCommunicationMessage, as: 'messages', required: false, attributes: ['id', 'body', 'direction', 'messageType', 'createdByUserId', 'createdAt'] }],
}],
order: [['deliveredAt', 'DESC']],
});
const items = recipients.map((recipient) => {
const thread = recipient.thread;
const messages = (thread?.messages || [])
.filter((message) => message.messageType === 'message' && (message.direction === 'outbound' || (message.direction === 'inbound' && Number(message.createdByUserId) === userId)))
.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt))
.map((message) => ({ id: message.id, body: message.body, direction: message.direction, createdAt: message.createdAt }));
const latestOutbound = [...messages].reverse().find((message) => message.direction === 'outbound');
return {
recipientId: recipient.id,
threadId: thread.id,
subject: thread.subject,
threadType: thread.threadType,
sentAt: recipient.deliveredAt || thread.sentAt,
readAt: recipient.readAt,
canAskQuestion: Boolean(thread.replyTo),
preview: latestOutbound?.body || '',
messages,
};
});
res.json({ items, unreadCount: items.filter((item) => !item.readAt).length });
} catch (error) {
console.error('[getMemberInbox] - Error:', error);
res.status(500).json({ error: 'Nachrichten konnten nicht geladen werden.' });
}
};
const markInboxItemRead = async (req, res) => {
try {
const clubId = Number(req.params.clubId);
const recipientId = Number(req.params.recipientId);
const access = await getApprovedLinkedMember(clubId, Number(req.user?.id));
if (access.error) return res.status(access.status).json({ error: access.error });
const recipient = await ClubCommunicationRecipient.findOne({ where: { id: recipientId, clubId, memberId: access.member.id, deliveryStatus: 'sent' } });
if (!recipient) return res.status(404).json({ error: 'Nachricht nicht gefunden.' });
if (!recipient.readAt) await recipient.update({ readAt: new Date() });
res.json({ recipientId, readAt: recipient.readAt });
} catch (error) {
console.error('[markInboxItemRead] - Error:', error);
res.status(500).json({ error: 'Lesestatus konnte nicht gespeichert werden.' });
}
};
const askInboxQuestion = async (req, res) => {
try {
const clubId = Number(req.params.clubId);
const recipientId = Number(req.params.recipientId);
const userId = Number(req.user?.id);
const body = String(req.body?.body || '').trim();
if (!body) return res.status(400).json({ error: 'Bitte formuliere deine Rückfrage.' });
if (body.length > 4000) return res.status(400).json({ error: 'Die Rückfrage ist zu lang.' });
const access = await getApprovedLinkedMember(clubId, userId);
if (access.error) return res.status(access.status).json({ error: access.error });
const recipient = await ClubCommunicationRecipient.findOne({
where: { id: recipientId, clubId, memberId: access.member.id, deliveryStatus: 'sent' },
include: [{ model: ClubCommunicationThread, as: 'thread', required: true, attributes: ['id', 'status', 'replyTo'] }],
});
if (!recipient?.thread || recipient.thread.status !== 'sent' || !recipient.thread.replyTo) return res.status(409).json({ error: 'Für diese Nachricht ist keine Rückfrage vorgesehen.' });
const message = await ClubCommunicationMessage.create({ threadId: recipient.thread.id, clubId, body, direction: 'inbound', messageType: 'message', createdByUserId: userId });
if (!recipient.readAt) await recipient.update({ readAt: new Date() });
res.status(201).json({ id: message.id, body: message.body, direction: message.direction, createdAt: message.createdAt });
} catch (error) {
console.error('[askInboxQuestion] - Error:', error);
res.status(500).json({ error: 'Rückfrage konnte nicht gesendet werden.' });
}
};
export {
getClubMembers,
getWaitingApprovals,
@@ -563,5 +648,8 @@ export {
updateOwnTrainingAttendance,
getEventResponses,
getProfileChangeRequests,
reviewProfileChangeRequest
reviewProfileChangeRequest,
getMemberInbox,
markInboxItemRead,
askInboxQuestion
};