2025-11-21 21:00:50 +01:00
|
|
|
/**
|
|
|
|
|
* Dashboard Routes
|
|
|
|
|
* Aggregated data for user dashboard
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const express = require('express');
|
|
|
|
|
const router = express.Router();
|
|
|
|
|
const { authenticate } = require('../middleware/auth');
|
|
|
|
|
const { PrismaClient } = require('@prisma/client');
|
2025-11-21 21:41:16 +01:00
|
|
|
const { getEventsOnlineCounts } = require('../socket');
|
2025-11-23 22:40:54 +01:00
|
|
|
const { MATCH_STATUS } = require('../constants');
|
2025-11-21 21:00:50 +01:00
|
|
|
|
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* GET /api/dashboard
|
|
|
|
|
* Get dashboard data for authenticated user
|
|
|
|
|
*
|
|
|
|
|
* Returns:
|
|
|
|
|
* - activeEvents: Events user is checked in to
|
|
|
|
|
* - activeMatches: Accepted matches with video/rating status
|
|
|
|
|
* - matchRequests: Pending incoming/outgoing match requests
|
|
|
|
|
*/
|
|
|
|
|
router.get('/', authenticate, async (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const userId = req.user.id;
|
|
|
|
|
|
|
|
|
|
// 1. Get active events (user is participant)
|
|
|
|
|
const eventParticipants = await prisma.eventParticipant.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
userId: userId,
|
|
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
event: true,
|
|
|
|
|
},
|
|
|
|
|
orderBy: {
|
|
|
|
|
event: {
|
|
|
|
|
startDate: 'asc', // Upcoming events first
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-21 21:41:16 +01:00
|
|
|
// Get online counts for all events (safely handle if socket not initialized)
|
|
|
|
|
let onlineCounts = {};
|
|
|
|
|
try {
|
|
|
|
|
const eventIds = eventParticipants.map(ep => ep.event.id);
|
|
|
|
|
onlineCounts = getEventsOnlineCounts(eventIds);
|
|
|
|
|
} catch (_) {
|
|
|
|
|
// Socket may not be initialized (e.g., during tests)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-30 15:14:06 +01:00
|
|
|
// Get user's heats and recording suggestions for each event
|
2025-11-21 21:00:50 +01:00
|
|
|
const activeEvents = await Promise.all(
|
|
|
|
|
eventParticipants.map(async (ep) => {
|
|
|
|
|
const heats = await prisma.eventUserHeat.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
userId: userId,
|
|
|
|
|
eventId: ep.event.id,
|
|
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
division: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
|
|
|
|
abbreviation: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
competitionType: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
|
|
|
|
abbreviation: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-30 15:14:06 +01:00
|
|
|
// Get recording suggestions for this event
|
|
|
|
|
const heatIds = heats.map(h => h.id);
|
|
|
|
|
|
|
|
|
|
// Suggestions where user is the dancer (someone records them)
|
|
|
|
|
const toBeRecorded = await prisma.recordingSuggestion.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
eventId: ep.event.id,
|
|
|
|
|
heatId: { in: heatIds },
|
|
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
heat: {
|
|
|
|
|
include: {
|
|
|
|
|
division: { select: { abbreviation: true } },
|
|
|
|
|
competitionType: { select: { abbreviation: true } },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
recorder: {
|
|
|
|
|
select: { id: true, username: true, avatar: true },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Suggestions where user is the recorder (they record someone)
|
|
|
|
|
const toRecord = await prisma.recordingSuggestion.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
eventId: ep.event.id,
|
|
|
|
|
recorderId: userId,
|
|
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
heat: {
|
|
|
|
|
include: {
|
|
|
|
|
division: { select: { abbreviation: true } },
|
|
|
|
|
competitionType: { select: { abbreviation: true } },
|
|
|
|
|
user: {
|
|
|
|
|
select: { id: true, username: true, avatar: true },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-21 21:00:50 +01:00
|
|
|
return {
|
|
|
|
|
id: ep.event.id,
|
|
|
|
|
slug: ep.event.slug,
|
|
|
|
|
name: ep.event.name,
|
|
|
|
|
location: ep.event.location,
|
|
|
|
|
startDate: ep.event.startDate,
|
|
|
|
|
endDate: ep.event.endDate,
|
|
|
|
|
participantsCount: ep.event.participantsCount,
|
2025-11-21 21:41:16 +01:00
|
|
|
onlineCount: onlineCounts[ep.event.id] || 0,
|
2025-11-21 21:00:50 +01:00
|
|
|
myHeats: heats.map((h) => ({
|
|
|
|
|
id: h.id,
|
|
|
|
|
competitionType: h.competitionType,
|
|
|
|
|
division: h.division,
|
|
|
|
|
heatNumber: h.heatNumber,
|
|
|
|
|
role: h.role,
|
|
|
|
|
})),
|
2025-11-30 15:14:06 +01:00
|
|
|
recordingSuggestions: {
|
|
|
|
|
toBeRecorded: toBeRecorded.map(s => ({
|
|
|
|
|
id: s.id,
|
|
|
|
|
status: s.status,
|
|
|
|
|
heat: {
|
|
|
|
|
heatNumber: s.heat.heatNumber,
|
|
|
|
|
division: s.heat.division?.abbreviation,
|
|
|
|
|
competitionType: s.heat.competitionType?.abbreviation,
|
|
|
|
|
},
|
|
|
|
|
recorder: s.recorder,
|
|
|
|
|
})),
|
|
|
|
|
toRecord: toRecord.map(s => ({
|
|
|
|
|
id: s.id,
|
|
|
|
|
status: s.status,
|
|
|
|
|
heat: {
|
|
|
|
|
heatNumber: s.heat.heatNumber,
|
|
|
|
|
division: s.heat.division?.abbreviation,
|
|
|
|
|
competitionType: s.heat.competitionType?.abbreviation,
|
|
|
|
|
},
|
|
|
|
|
dancer: s.heat.user,
|
|
|
|
|
})),
|
|
|
|
|
},
|
2025-11-21 21:00:50 +01:00
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 2. Get active matches (accepted status)
|
|
|
|
|
const matches = await prisma.match.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
OR: [
|
|
|
|
|
{ user1Id: userId },
|
|
|
|
|
{ user2Id: userId },
|
|
|
|
|
],
|
2025-11-23 22:40:54 +01:00
|
|
|
status: MATCH_STATUS.ACCEPTED,
|
2025-11-21 21:00:50 +01:00
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
user1: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
username: true,
|
|
|
|
|
firstName: true,
|
|
|
|
|
lastName: true,
|
|
|
|
|
avatar: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
user2: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
username: true,
|
|
|
|
|
firstName: true,
|
|
|
|
|
lastName: true,
|
|
|
|
|
avatar: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
event: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
|
|
|
|
slug: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
room: {
|
|
|
|
|
include: {
|
|
|
|
|
messages: {
|
|
|
|
|
orderBy: {
|
|
|
|
|
createdAt: 'desc',
|
|
|
|
|
},
|
|
|
|
|
take: 50, // Get recent messages to check for videos
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
ratings: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const activeMatches = matches.map((match) => {
|
|
|
|
|
// Determine partner (the other user)
|
|
|
|
|
const isUser1 = match.user1Id === userId;
|
|
|
|
|
const partner = isUser1 ? match.user2 : match.user1;
|
|
|
|
|
const partnerId = partner.id;
|
|
|
|
|
|
|
|
|
|
// Check video exchange status from messages
|
|
|
|
|
const myVideoMessages = match.room?.messages.filter(
|
|
|
|
|
(m) => m.userId === userId && m.content.includes('📹 Video sent:')
|
|
|
|
|
) || [];
|
|
|
|
|
const partnerVideoMessages = match.room?.messages.filter(
|
|
|
|
|
(m) => m.userId === partnerId && m.content.includes('📹 Video sent:')
|
|
|
|
|
) || [];
|
|
|
|
|
|
|
|
|
|
const videoExchange = {
|
|
|
|
|
sentByMe: myVideoMessages.length > 0,
|
|
|
|
|
receivedFromPartner: partnerVideoMessages.length > 0,
|
|
|
|
|
lastVideoTimestamp: myVideoMessages.length > 0 || partnerVideoMessages.length > 0
|
|
|
|
|
? new Date(Math.max(
|
|
|
|
|
...[...myVideoMessages, ...partnerVideoMessages].map(m => m.createdAt.getTime())
|
|
|
|
|
))
|
|
|
|
|
: null,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Check rating status
|
|
|
|
|
const myRating = match.ratings.find((r) => r.raterId === userId);
|
|
|
|
|
const partnerRating = match.ratings.find((r) => r.raterId === partnerId);
|
|
|
|
|
|
|
|
|
|
const ratings = {
|
|
|
|
|
ratedByMe: !!myRating,
|
|
|
|
|
ratedByPartner: !!partnerRating,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Get last message timestamp
|
|
|
|
|
const lastMessage = match.room?.messages[0];
|
|
|
|
|
const lastMessageAt = lastMessage ? lastMessage.createdAt : match.createdAt;
|
|
|
|
|
|
2025-11-21 21:46:00 +01:00
|
|
|
// Calculate unread count
|
|
|
|
|
const myLastReadAt = isUser1 ? match.user1LastReadAt : match.user2LastReadAt;
|
|
|
|
|
const unreadCount = myLastReadAt
|
|
|
|
|
? (match.room?.messages || []).filter(
|
|
|
|
|
(m) => m.userId !== userId && new Date(m.createdAt) > new Date(myLastReadAt)
|
|
|
|
|
).length
|
|
|
|
|
: (match.room?.messages || []).filter((m) => m.userId !== userId).length;
|
|
|
|
|
|
2025-11-21 21:00:50 +01:00
|
|
|
return {
|
|
|
|
|
id: match.id,
|
|
|
|
|
slug: match.slug,
|
|
|
|
|
partner: {
|
|
|
|
|
id: partner.id,
|
|
|
|
|
username: partner.username,
|
|
|
|
|
firstName: partner.firstName,
|
|
|
|
|
lastName: partner.lastName,
|
|
|
|
|
avatar: partner.avatar,
|
|
|
|
|
},
|
|
|
|
|
event: {
|
|
|
|
|
id: match.event.id,
|
|
|
|
|
name: match.event.name,
|
|
|
|
|
slug: match.event.slug,
|
|
|
|
|
},
|
|
|
|
|
videoExchange,
|
|
|
|
|
ratings,
|
2025-11-21 21:46:00 +01:00
|
|
|
unreadCount,
|
2025-11-21 21:00:50 +01:00
|
|
|
lastMessageAt,
|
|
|
|
|
status: match.status,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Sort by lastMessageAt (most recent first)
|
|
|
|
|
activeMatches.sort((a, b) => new Date(b.lastMessageAt) - new Date(a.lastMessageAt));
|
|
|
|
|
|
|
|
|
|
// 3. Get match requests
|
|
|
|
|
// Incoming: user is user2, status is pending
|
|
|
|
|
const incomingRequests = await prisma.match.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
user2Id: userId,
|
2025-11-23 22:40:54 +01:00
|
|
|
status: MATCH_STATUS.PENDING,
|
2025-11-21 21:00:50 +01:00
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
user1: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
username: true,
|
|
|
|
|
firstName: true,
|
|
|
|
|
lastName: true,
|
|
|
|
|
avatar: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
event: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
|
|
|
|
slug: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
orderBy: {
|
|
|
|
|
createdAt: 'desc',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Get requester heats for incoming requests
|
|
|
|
|
const incoming = await Promise.all(
|
|
|
|
|
incomingRequests.map(async (req) => {
|
|
|
|
|
const requesterHeats = await prisma.eventUserHeat.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
userId: req.user1Id,
|
|
|
|
|
eventId: req.eventId,
|
|
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
division: {
|
|
|
|
|
select: {
|
|
|
|
|
name: true,
|
|
|
|
|
abbreviation: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
competitionType: {
|
|
|
|
|
select: {
|
|
|
|
|
name: true,
|
|
|
|
|
abbreviation: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: req.id,
|
|
|
|
|
slug: req.slug,
|
|
|
|
|
requester: {
|
|
|
|
|
id: req.user1.id,
|
|
|
|
|
username: req.user1.username,
|
|
|
|
|
firstName: req.user1.firstName,
|
|
|
|
|
lastName: req.user1.lastName,
|
|
|
|
|
avatar: req.user1.avatar,
|
|
|
|
|
},
|
|
|
|
|
event: {
|
|
|
|
|
id: req.event.id,
|
|
|
|
|
name: req.event.name,
|
|
|
|
|
slug: req.event.slug,
|
|
|
|
|
},
|
|
|
|
|
requesterHeats: requesterHeats.map((h) => ({
|
|
|
|
|
competitionType: h.competitionType,
|
|
|
|
|
division: h.division,
|
|
|
|
|
heatNumber: h.heatNumber,
|
|
|
|
|
role: h.role,
|
|
|
|
|
})),
|
|
|
|
|
createdAt: req.createdAt,
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Outgoing: user is user1, status is pending
|
|
|
|
|
const outgoingRequests = await prisma.match.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
user1Id: userId,
|
2025-11-23 22:40:54 +01:00
|
|
|
status: MATCH_STATUS.PENDING,
|
2025-11-21 21:00:50 +01:00
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
user2: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
username: true,
|
|
|
|
|
firstName: true,
|
|
|
|
|
lastName: true,
|
|
|
|
|
avatar: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
event: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
|
|
|
|
slug: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
orderBy: {
|
|
|
|
|
createdAt: 'desc',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const outgoing = outgoingRequests.map((req) => ({
|
|
|
|
|
id: req.id,
|
|
|
|
|
slug: req.slug,
|
|
|
|
|
recipient: {
|
|
|
|
|
id: req.user2.id,
|
|
|
|
|
username: req.user2.username,
|
|
|
|
|
firstName: req.user2.firstName,
|
|
|
|
|
lastName: req.user2.lastName,
|
|
|
|
|
avatar: req.user2.avatar,
|
|
|
|
|
},
|
|
|
|
|
event: {
|
|
|
|
|
id: req.event.id,
|
|
|
|
|
name: req.event.name,
|
|
|
|
|
slug: req.event.slug,
|
|
|
|
|
},
|
|
|
|
|
createdAt: req.createdAt,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
success: true,
|
|
|
|
|
data: {
|
|
|
|
|
activeEvents,
|
|
|
|
|
activeMatches,
|
|
|
|
|
matchRequests: {
|
|
|
|
|
incoming,
|
|
|
|
|
outgoing,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
next(error);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
module.exports = router;
|