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:
@@ -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);
|
||||
|
||||
@@ -47,6 +47,15 @@ const MatchCard = ({ match, onAccept, onReject, onOpenChat, processing }) => {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{match.type === 'auto' ? (
|
||||
<span className="text-xs px-2 py-1 bg-purple-100 text-purple-700 rounded-full font-medium">
|
||||
Auto
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs px-2 py-1 bg-gray-100 text-gray-700 rounded-full font-medium">
|
||||
Manual
|
||||
</span>
|
||||
)}
|
||||
{isIncoming && (
|
||||
<span className="text-xs px-2 py-1 bg-amber-100 text-amber-700 rounded-full font-medium">
|
||||
Incoming Request
|
||||
@@ -66,7 +75,7 @@ const MatchCard = ({ match, onAccept, onReject, onOpenChat, processing }) => {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
{isIncoming && (
|
||||
{isIncoming && match.type === 'manual' && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => onAccept(match.slug)}
|
||||
@@ -91,7 +100,16 @@ const MatchCard = ({ match, onAccept, onReject, onOpenChat, processing }) => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{isOutgoing && (
|
||||
{isIncoming && match.type === 'auto' && (
|
||||
<Link
|
||||
to={`/events/${match.event.slug}/chat?tab=records`}
|
||||
className="px-3 py-1.5 text-sm bg-purple-600 text-white rounded-md hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
Go to Records
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{isOutgoing && match.type === 'manual' && (
|
||||
<button
|
||||
onClick={() => onReject(match.slug)}
|
||||
disabled={processing}
|
||||
@@ -101,7 +119,16 @@ const MatchCard = ({ match, onAccept, onReject, onOpenChat, processing }) => {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isAccepted && (
|
||||
{isOutgoing && match.type === 'auto' && (
|
||||
<Link
|
||||
to={`/events/${match.event.slug}/chat?tab=records`}
|
||||
className="px-3 py-1.5 text-sm bg-purple-600 text-white rounded-md hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
Go to Records
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{isAccepted && match.slug && (
|
||||
<button
|
||||
onClick={() => onOpenChat(match)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors"
|
||||
@@ -110,6 +137,12 @@ const MatchCard = ({ match, onAccept, onReject, onOpenChat, processing }) => {
|
||||
Open Chat
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isAccepted && !match.slug && (
|
||||
<span className="text-sm text-gray-500 italic">
|
||||
Chat not available yet
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user