feat(backend): implement dashboard API endpoint
- Add GET /api/dashboard endpoint for authenticated users - Returns active events with user heats - Returns accepted matches with partner info - Detects video exchange status from message parsing - Tracks rating completion status (rated by me/partner) - Returns incoming/outgoing pending match requests - Add comprehensive test suite (12 tests, 93% coverage) - Add DASHBOARD_PLAN.md with full design documentation
This commit is contained in:
501
backend/src/__tests__/dashboard.test.js
Normal file
501
backend/src/__tests__/dashboard.test.js
Normal file
@@ -0,0 +1,501 @@
|
||||
/**
|
||||
* Dashboard API Tests
|
||||
* Tests for GET /api/dashboard endpoint
|
||||
*
|
||||
* @group dashboard
|
||||
*/
|
||||
|
||||
const request = require('supertest');
|
||||
const app = require('../app');
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const { generateToken } = require('../utils/auth');
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
describe('Dashboard API', () => {
|
||||
let testUser;
|
||||
let testUser2;
|
||||
let testEvent;
|
||||
let authToken;
|
||||
let authToken2;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Clean up test data
|
||||
await prisma.rating.deleteMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ rater: { username: { startsWith: 'dashboard_' } } },
|
||||
{ rated: { username: { startsWith: 'dashboard_' } } }
|
||||
]
|
||||
}
|
||||
});
|
||||
await prisma.eventUserHeat.deleteMany({
|
||||
where: { user: { username: { startsWith: 'dashboard_' } } }
|
||||
});
|
||||
await prisma.match.deleteMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ user1: { username: { startsWith: 'dashboard_' } } },
|
||||
{ user2: { username: { startsWith: 'dashboard_' } } }
|
||||
]
|
||||
}
|
||||
});
|
||||
await prisma.message.deleteMany({
|
||||
where: { user: { username: { startsWith: 'dashboard_' } } }
|
||||
});
|
||||
await prisma.eventParticipant.deleteMany({
|
||||
where: { user: { username: { startsWith: 'dashboard_' } } }
|
||||
});
|
||||
await prisma.event.deleteMany({
|
||||
where: { slug: { startsWith: 'dashboard-' } }
|
||||
});
|
||||
await prisma.user.deleteMany({
|
||||
where: { username: { startsWith: 'dashboard_' } }
|
||||
});
|
||||
|
||||
// Create test users
|
||||
testUser = await prisma.user.create({
|
||||
data: {
|
||||
username: 'dashboard_user1',
|
||||
email: 'dashboard_user1@test.com',
|
||||
passwordHash: 'hash123',
|
||||
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=dashboard1',
|
||||
},
|
||||
});
|
||||
|
||||
testUser2 = await prisma.user.create({
|
||||
data: {
|
||||
username: 'dashboard_user2',
|
||||
email: 'dashboard_user2@test.com',
|
||||
passwordHash: 'hash456',
|
||||
firstName: 'Sarah',
|
||||
lastName: 'Martinez',
|
||||
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=dashboard2',
|
||||
},
|
||||
});
|
||||
|
||||
// Create test event
|
||||
testEvent = await prisma.event.create({
|
||||
data: {
|
||||
slug: 'dashboard-test-event',
|
||||
name: 'Dashboard Test Event',
|
||||
location: 'Test City',
|
||||
startDate: new Date('2025-12-01'),
|
||||
endDate: new Date('2025-12-03'),
|
||||
participantsCount: 2,
|
||||
},
|
||||
});
|
||||
|
||||
// Create chat room for the event
|
||||
await prisma.chatRoom.create({
|
||||
data: {
|
||||
eventId: testEvent.id,
|
||||
type: 'event',
|
||||
},
|
||||
});
|
||||
|
||||
// Generate auth tokens
|
||||
authToken = generateToken({ userId: testUser.id });
|
||||
authToken2 = generateToken({ userId: testUser2.id });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Clean up test data
|
||||
await prisma.rating.deleteMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ rater: { username: { startsWith: 'dashboard_' } } },
|
||||
{ rated: { username: { startsWith: 'dashboard_' } } }
|
||||
]
|
||||
}
|
||||
});
|
||||
await prisma.eventUserHeat.deleteMany({
|
||||
where: { user: { username: { startsWith: 'dashboard_' } } }
|
||||
});
|
||||
await prisma.match.deleteMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ user1: { username: { startsWith: 'dashboard_' } } },
|
||||
{ user2: { username: { startsWith: 'dashboard_' } } }
|
||||
]
|
||||
}
|
||||
});
|
||||
await prisma.message.deleteMany({
|
||||
where: { user: { username: { startsWith: 'dashboard_' } } }
|
||||
});
|
||||
await prisma.eventParticipant.deleteMany({
|
||||
where: { user: { username: { startsWith: 'dashboard_' } } }
|
||||
});
|
||||
await prisma.event.deleteMany({
|
||||
where: { slug: { startsWith: 'dashboard-' } }
|
||||
});
|
||||
await prisma.user.deleteMany({
|
||||
where: { username: { startsWith: 'dashboard_' } }
|
||||
});
|
||||
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
|
||||
describe('GET /api/dashboard', () => {
|
||||
it('should require authentication', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/dashboard');
|
||||
|
||||
expect(res.status).toBe(401);
|
||||
expect(res.body.error).toBe('Unauthorized');
|
||||
});
|
||||
|
||||
it('should return empty dashboard for new user', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/dashboard')
|
||||
.set('Authorization', `Bearer ${authToken}`);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.data).toBeDefined();
|
||||
expect(res.body.data.activeEvents).toEqual([]);
|
||||
expect(res.body.data.activeMatches).toEqual([]);
|
||||
expect(res.body.data.matchRequests.incoming).toEqual([]);
|
||||
expect(res.body.data.matchRequests.outgoing).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return active events when user is participant', async () => {
|
||||
// Check in user to event
|
||||
await prisma.eventParticipant.create({
|
||||
data: {
|
||||
userId: testUser.id,
|
||||
eventId: testEvent.id,
|
||||
},
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.get('/api/dashboard')
|
||||
.set('Authorization', `Bearer ${authToken}`);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.data.activeEvents).toHaveLength(1);
|
||||
expect(res.body.data.activeEvents[0]).toMatchObject({
|
||||
id: testEvent.id,
|
||||
slug: testEvent.slug,
|
||||
name: testEvent.name,
|
||||
location: testEvent.location,
|
||||
participantsCount: expect.any(Number),
|
||||
myHeats: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should include user heats in active events', async () => {
|
||||
// Get divisions and competition types
|
||||
const division = await prisma.division.findFirst();
|
||||
const competitionType = await prisma.competitionType.findFirst();
|
||||
|
||||
// Create heat for user
|
||||
await prisma.eventUserHeat.create({
|
||||
data: {
|
||||
userId: testUser.id,
|
||||
eventId: testEvent.id,
|
||||
divisionId: division.id,
|
||||
competitionTypeId: competitionType.id,
|
||||
heatNumber: 1,
|
||||
role: 'Leader',
|
||||
},
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.get('/api/dashboard')
|
||||
.set('Authorization', `Bearer ${authToken}`);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.data.activeEvents[0].myHeats).toHaveLength(1);
|
||||
expect(res.body.data.activeEvents[0].myHeats[0]).toMatchObject({
|
||||
heatNumber: 1,
|
||||
role: 'Leader',
|
||||
competitionType: expect.objectContaining({
|
||||
name: expect.any(String),
|
||||
abbreviation: expect.any(String),
|
||||
}),
|
||||
division: expect.objectContaining({
|
||||
name: expect.any(String),
|
||||
abbreviation: expect.any(String),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return active matches with partner info', async () => {
|
||||
// Create match room
|
||||
const matchRoom = await prisma.chatRoom.create({
|
||||
data: {
|
||||
eventId: testEvent.id,
|
||||
type: 'private',
|
||||
},
|
||||
});
|
||||
|
||||
// Create accepted match
|
||||
const match = await prisma.match.create({
|
||||
data: {
|
||||
user1Id: testUser.id,
|
||||
user2Id: testUser2.id,
|
||||
eventId: testEvent.id,
|
||||
roomId: matchRoom.id,
|
||||
status: 'accepted',
|
||||
},
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.get('/api/dashboard')
|
||||
.set('Authorization', `Bearer ${authToken}`);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.data.activeMatches).toHaveLength(1);
|
||||
expect(res.body.data.activeMatches[0]).toMatchObject({
|
||||
id: match.id,
|
||||
slug: match.slug,
|
||||
partner: {
|
||||
id: testUser2.id,
|
||||
username: testUser2.username,
|
||||
firstName: testUser2.firstName,
|
||||
lastName: testUser2.lastName,
|
||||
avatar: testUser2.avatar,
|
||||
},
|
||||
event: {
|
||||
id: testEvent.id,
|
||||
name: testEvent.name,
|
||||
},
|
||||
status: 'accepted',
|
||||
});
|
||||
});
|
||||
|
||||
it('should detect video exchange status from messages', async () => {
|
||||
// Get the match
|
||||
const match = await prisma.match.findFirst({
|
||||
where: {
|
||||
user1Id: testUser.id,
|
||||
user2Id: testUser2.id,
|
||||
},
|
||||
include: { room: true },
|
||||
});
|
||||
|
||||
// User1 sent video to User2
|
||||
await prisma.message.create({
|
||||
data: {
|
||||
roomId: match.roomId,
|
||||
userId: testUser.id,
|
||||
content: '📹 Video sent: dance.mp4 (50.00 MB)',
|
||||
type: 'text',
|
||||
},
|
||||
});
|
||||
|
||||
// User2 sent video to User1
|
||||
await prisma.message.create({
|
||||
data: {
|
||||
roomId: match.roomId,
|
||||
userId: testUser2.id,
|
||||
content: '📹 Video sent: performance.mp4 (75.50 MB)',
|
||||
type: 'text',
|
||||
},
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.get('/api/dashboard')
|
||||
.set('Authorization', `Bearer ${authToken}`);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.data.activeMatches[0].videoExchange).toMatchObject({
|
||||
sentByMe: true,
|
||||
receivedFromPartner: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should detect rating status', async () => {
|
||||
const match = await prisma.match.findFirst({
|
||||
where: {
|
||||
user1Id: testUser.id,
|
||||
user2Id: testUser2.id,
|
||||
},
|
||||
});
|
||||
|
||||
// User1 rated User2
|
||||
await prisma.rating.create({
|
||||
data: {
|
||||
matchId: match.id,
|
||||
raterId: testUser.id,
|
||||
ratedId: testUser2.id,
|
||||
score: 5,
|
||||
wouldCollaborateAgain: true,
|
||||
},
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.get('/api/dashboard')
|
||||
.set('Authorization', `Bearer ${authToken}`);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.data.activeMatches[0].ratings).toMatchObject({
|
||||
ratedByMe: true,
|
||||
ratedByPartner: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return incoming match requests', async () => {
|
||||
// Create pending match where testUser is user2 (recipient)
|
||||
const pendingMatch = await prisma.match.create({
|
||||
data: {
|
||||
user1Id: testUser2.id,
|
||||
user2Id: testUser.id,
|
||||
eventId: testEvent.id,
|
||||
status: 'pending',
|
||||
},
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.get('/api/dashboard')
|
||||
.set('Authorization', `Bearer ${authToken}`);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.data.matchRequests.incoming).toHaveLength(1);
|
||||
expect(res.body.data.matchRequests.incoming[0]).toMatchObject({
|
||||
id: pendingMatch.id,
|
||||
slug: pendingMatch.slug,
|
||||
requester: {
|
||||
id: testUser2.id,
|
||||
username: testUser2.username,
|
||||
firstName: testUser2.firstName,
|
||||
lastName: testUser2.lastName,
|
||||
},
|
||||
event: {
|
||||
id: testEvent.id,
|
||||
name: testEvent.name,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return outgoing match requests', async () => {
|
||||
// Create another user for outgoing request
|
||||
const testUser3 = await prisma.user.create({
|
||||
data: {
|
||||
username: 'dashboard_user3',
|
||||
email: 'dashboard_user3@test.com',
|
||||
passwordHash: 'hash789',
|
||||
firstName: 'John',
|
||||
lastName: 'Dancer',
|
||||
},
|
||||
});
|
||||
|
||||
// Create pending match where testUser is user1 (requester)
|
||||
const outgoingMatch = await prisma.match.create({
|
||||
data: {
|
||||
user1Id: testUser.id,
|
||||
user2Id: testUser3.id,
|
||||
eventId: testEvent.id,
|
||||
status: 'pending',
|
||||
},
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.get('/api/dashboard')
|
||||
.set('Authorization', `Bearer ${authToken}`);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.data.matchRequests.outgoing).toHaveLength(1);
|
||||
expect(res.body.data.matchRequests.outgoing[0]).toMatchObject({
|
||||
id: outgoingMatch.id,
|
||||
slug: outgoingMatch.slug,
|
||||
recipient: {
|
||||
id: testUser3.id,
|
||||
username: testUser3.username,
|
||||
firstName: testUser3.firstName,
|
||||
lastName: testUser3.lastName,
|
||||
},
|
||||
event: {
|
||||
id: testEvent.id,
|
||||
name: testEvent.name,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should sort events by start date (upcoming first)', async () => {
|
||||
// Create another event with earlier start date
|
||||
const earlierEvent = await prisma.event.create({
|
||||
data: {
|
||||
slug: 'dashboard-earlier-event',
|
||||
name: 'Earlier Event',
|
||||
location: 'Test City 2',
|
||||
startDate: new Date('2025-11-25'),
|
||||
endDate: new Date('2025-11-27'),
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.chatRoom.create({
|
||||
data: {
|
||||
eventId: earlierEvent.id,
|
||||
type: 'event',
|
||||
},
|
||||
});
|
||||
|
||||
// Check in user to both events
|
||||
await prisma.eventParticipant.create({
|
||||
data: {
|
||||
userId: testUser.id,
|
||||
eventId: earlierEvent.id,
|
||||
},
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.get('/api/dashboard')
|
||||
.set('Authorization', `Bearer ${authToken}`);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.data.activeEvents).toHaveLength(2);
|
||||
// Earlier event should be first
|
||||
expect(res.body.data.activeEvents[0].slug).toBe('dashboard-earlier-event');
|
||||
expect(res.body.data.activeEvents[1].slug).toBe('dashboard-test-event');
|
||||
});
|
||||
|
||||
it('should handle user as user2 in match (partner detection)', async () => {
|
||||
// Get testUser2's dashboard (they are user2 in the match)
|
||||
const res = await request(app)
|
||||
.get('/api/dashboard')
|
||||
.set('Authorization', `Bearer ${authToken2}`);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const matches = res.body.data.activeMatches;
|
||||
|
||||
if (matches.length > 0) {
|
||||
// Partner should be testUser (user1)
|
||||
expect(matches[0].partner.id).toBe(testUser.id);
|
||||
expect(matches[0].partner.username).toBe(testUser.username);
|
||||
}
|
||||
});
|
||||
|
||||
it('should calculate lastMessageAt correctly', async () => {
|
||||
const match = await prisma.match.findFirst({
|
||||
where: {
|
||||
user1Id: testUser.id,
|
||||
user2Id: testUser2.id,
|
||||
status: 'accepted',
|
||||
},
|
||||
include: { room: true },
|
||||
});
|
||||
|
||||
// Add a regular message
|
||||
const now = new Date();
|
||||
await prisma.message.create({
|
||||
data: {
|
||||
roomId: match.roomId,
|
||||
userId: testUser.id,
|
||||
content: 'Hey, ready to practice?',
|
||||
type: 'text',
|
||||
createdAt: now,
|
||||
},
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.get('/api/dashboard')
|
||||
.set('Authorization', `Bearer ${authToken}`);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.data.activeMatches[0].lastMessageAt).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -111,6 +111,7 @@ app.use('/api/', apiLimiter);
|
||||
// API routes
|
||||
app.use('/api/auth', require('./routes/auth'));
|
||||
app.use('/api/users', require('./routes/users'));
|
||||
app.use('/api/dashboard', require('./routes/dashboard'));
|
||||
app.use('/api/events', require('./routes/events'));
|
||||
app.use('/api/wsdc', require('./routes/wsdc'));
|
||||
app.use('/api/divisions', require('./routes/divisions'));
|
||||
|
||||
340
backend/src/routes/dashboard.js
Normal file
340
backend/src/routes/dashboard.js
Normal file
@@ -0,0 +1,340 @@
|
||||
/**
|
||||
* 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');
|
||||
|
||||
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
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Get user's heats for each event
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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,
|
||||
myHeats: heats.map((h) => ({
|
||||
id: h.id,
|
||||
competitionType: h.competitionType,
|
||||
division: h.division,
|
||||
heatNumber: h.heatNumber,
|
||||
role: h.role,
|
||||
})),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// 2. Get active matches (accepted status)
|
||||
const matches = await prisma.match.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ user1Id: userId },
|
||||
{ user2Id: userId },
|
||||
],
|
||||
status: 'accepted',
|
||||
},
|
||||
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;
|
||||
|
||||
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,
|
||||
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,
|
||||
status: 'pending',
|
||||
},
|
||||
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,
|
||||
status: 'pending',
|
||||
},
|
||||
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;
|
||||
Reference in New Issue
Block a user