feat(matches): show both manual match requests and auto recording suggestions

Backend:
- Extend GET /api/matches to include RecordingSuggestions alongside Match objects
- Add 'type' field: 'manual' for user-created matches, 'auto' for algorithm suggestions
- Fetch suggestions where user is dancer (to be recorded) or recorder (recording others)
- Transform suggestions to match format with partner info
- Support status filtering for both types

Frontend:
- Display 'Auto' (purple) or 'Manual' (gray) badge on match cards
- For pending auto suggestions: show 'Go to Records' button instead of Accept/Reject
- For accepted auto suggestions without slug: show 'Chat not available yet'
- Only allow Accept/Reject actions on manual match requests
This commit is contained in:
Radosław Gierwiało
2025-11-30 15:30:49 +01:00
parent d8799d03af
commit f45cadae7d
2 changed files with 185 additions and 20 deletions

View File

@@ -177,15 +177,7 @@ router.get('/', authenticate, async (req, res, next) => {
const userId = req.user.id;
const { eventSlug, status } = req.query;
// Build where clause
const where = {
OR: [
{ user1Id: userId },
{ user2Id: userId },
],
};
// Filter by event if provided
let eventId = null;
if (eventSlug) {
const event = await prisma.event.findUnique({
where: { slug: eventSlug },
@@ -199,17 +191,28 @@ router.get('/', authenticate, async (req, res, next) => {
});
}
where.eventId = event.id;
eventId = event.id;
}
// Build where clause for matches
const matchWhere = {
OR: [
{ user1Id: userId },
{ user2Id: userId },
],
};
if (eventId) {
matchWhere.eventId = eventId;
}
// Filter by status if provided
if (status) {
where.status = status;
matchWhere.status = status;
}
// Fetch matches
// Fetch matches (manual match requests)
const matches = await prisma.match.findMany({
where,
where: matchWhere,
include: {
user1: {
select: {
@@ -250,7 +253,85 @@ router.get('/', authenticate, async (req, res, next) => {
},
});
// Transform matches to include partner info
// Fetch recording suggestions (auto matches from matching algorithm)
// Get user's heats first
const userHeats = await prisma.eventUserHeat.findMany({
where: {
userId: userId,
...(eventId ? { eventId } : {}),
},
select: {
id: true,
eventId: true,
},
});
const heatIds = userHeats.map(h => h.id);
// Fetch suggestions where user is dancer (to be recorded)
const suggestionsAsDancer = await prisma.recordingSuggestion.findMany({
where: {
heatId: { in: heatIds },
...(status ? { status } : {}),
},
include: {
recorder: {
select: {
id: true,
username: true,
avatar: true,
firstName: true,
lastName: true,
},
},
event: {
select: {
id: true,
slug: true,
name: true,
location: true,
startDate: true,
endDate: true,
},
},
},
});
// Fetch suggestions where user is recorder (recording others)
const suggestionsAsRecorder = await prisma.recordingSuggestion.findMany({
where: {
recorderId: userId,
...(eventId ? { eventId } : {}),
...(status ? { status } : {}),
},
include: {
heat: {
include: {
user: {
select: {
id: true,
username: true,
avatar: true,
firstName: true,
lastName: true,
},
},
},
},
event: {
select: {
id: true,
slug: true,
name: true,
location: true,
startDate: true,
endDate: true,
},
},
},
});
// Transform matches to include partner info and type
const transformedMatches = matches.map(match => {
const isUser1 = match.user1Id === userId;
const partner = isUser1 ? match.user2 : match.user1;
@@ -259,6 +340,7 @@ router.get('/', authenticate, async (req, res, next) => {
return {
id: match.id,
slug: match.slug,
type: 'manual',
partner: {
id: partner.id,
username: partner.username,
@@ -274,10 +356,60 @@ router.get('/', authenticate, async (req, res, next) => {
};
});
// Transform recording suggestions to match format
const transformedSuggestionsAsDancer = suggestionsAsDancer
.filter(s => s.recorder) // Only include if recorder exists
.map(suggestion => ({
id: `suggestion-${suggestion.id}`,
slug: null,
type: 'auto',
partner: {
id: suggestion.recorder.id,
username: suggestion.recorder.username,
avatar: suggestion.recorder.avatar,
firstName: suggestion.recorder.firstName,
lastName: suggestion.recorder.lastName,
},
event: suggestion.event,
status: suggestion.status,
roomId: null,
isInitiator: false,
createdAt: suggestion.createdAt,
suggestionId: suggestion.id,
}));
const transformedSuggestionsAsRecorder = suggestionsAsRecorder
.filter(s => s.heat?.user) // Only include if dancer exists
.map(suggestion => ({
id: `suggestion-${suggestion.id}`,
slug: null,
type: 'auto',
partner: {
id: suggestion.heat.user.id,
username: suggestion.heat.user.username,
avatar: suggestion.heat.user.avatar,
firstName: suggestion.heat.user.firstName,
lastName: suggestion.heat.user.lastName,
},
event: suggestion.event,
status: suggestion.status,
roomId: null,
isInitiator: true,
createdAt: suggestion.createdAt,
suggestionId: suggestion.id,
}));
// Combine all and sort by createdAt
const allItems = [
...transformedMatches,
...transformedSuggestionsAsDancer,
...transformedSuggestionsAsRecorder,
].sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
res.json({
success: true,
count: transformedMatches.length,
data: transformedMatches,
count: allItems.length,
data: allItems,
});
} catch (error) {
next(error);