feat: Implement member inbox with read status and question functionality
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 53s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 53s
This commit is contained in:
@@ -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
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE club_communication_recipients
|
||||
ADD COLUMN read_at DATETIME NULL AFTER delivered_at,
|
||||
ADD INDEX club_communication_recipients_member_read (club_id, member_id, read_at);
|
||||
@@ -44,6 +44,11 @@ const ClubCommunicationRecipient = sequelize.define('ClubCommunicationRecipient'
|
||||
allowNull: true,
|
||||
field: 'delivered_at',
|
||||
},
|
||||
readAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
field: 'read_at',
|
||||
},
|
||||
lastAttemptAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
|
||||
@@ -27,7 +27,10 @@ import {
|
||||
updateOwnTrainingAttendance,
|
||||
getEventResponses,
|
||||
getProfileChangeRequests,
|
||||
reviewProfileChangeRequest
|
||||
reviewProfileChangeRequest,
|
||||
getMemberInbox,
|
||||
markInboxItemRead,
|
||||
askInboxQuestion
|
||||
} from '../controllers/memberController.js';
|
||||
import express from 'express';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
@@ -55,6 +58,9 @@ router.put('/dashboard/:clubId/training/:diaryDateId/attendance', authenticate,
|
||||
router.get('/event-responses/:clubId/:eventId', authenticate, authorize('schedule', 'read'), getEventResponses);
|
||||
router.get('/profile-change-requests/:clubId', authenticate, authorize('members', 'read'), getProfileChangeRequests);
|
||||
router.patch('/profile-change-requests/:clubId/:requestId', authenticate, authorize('members', 'write'), reviewProfileChangeRequest);
|
||||
router.get('/inbox/:clubId', authenticate, getMemberInbox);
|
||||
router.patch('/inbox/:clubId/:recipientId/read', authenticate, markInboxItemRead);
|
||||
router.post('/inbox/:clubId/:recipientId/questions', authenticate, askInboxQuestion);
|
||||
router.get('/sepa/:clubId/:memberId', authenticate, authorize('members', 'read'), getMemberSepaMandate);
|
||||
router.put('/sepa/:clubId/:memberId', authenticate, authorize('members', 'write'), saveMemberSepaMandate);
|
||||
router.get('/play-interest/:clubId', authenticate, authorize('members', 'read'), getMemberPlayInterests);
|
||||
|
||||
@@ -648,6 +648,7 @@ class ClubCommunicationService {
|
||||
await recipient.update({
|
||||
deliveryStatus: 'sent',
|
||||
deliveredAt: now,
|
||||
readAt: null,
|
||||
lastAttemptAt: now,
|
||||
attemptCount: nextAttemptNo,
|
||||
retryable: false,
|
||||
|
||||
Reference in New Issue
Block a user