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 userId = req.user.id;
|
||||||
const { eventSlug, status } = req.query;
|
const { eventSlug, status } = req.query;
|
||||||
|
|
||||||
// Build where clause
|
let eventId = null;
|
||||||
const where = {
|
|
||||||
OR: [
|
|
||||||
{ user1Id: userId },
|
|
||||||
{ user2Id: userId },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter by event if provided
|
|
||||||
if (eventSlug) {
|
if (eventSlug) {
|
||||||
const event = await prisma.event.findUnique({
|
const event = await prisma.event.findUnique({
|
||||||
where: { slug: eventSlug },
|
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) {
|
if (status) {
|
||||||
where.status = status;
|
matchWhere.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch matches
|
// Fetch matches (manual match requests)
|
||||||
const matches = await prisma.match.findMany({
|
const matches = await prisma.match.findMany({
|
||||||
where,
|
where: matchWhere,
|
||||||
include: {
|
include: {
|
||||||
user1: {
|
user1: {
|
||||||
select: {
|
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 transformedMatches = matches.map(match => {
|
||||||
const isUser1 = match.user1Id === userId;
|
const isUser1 = match.user1Id === userId;
|
||||||
const partner = isUser1 ? match.user2 : match.user1;
|
const partner = isUser1 ? match.user2 : match.user1;
|
||||||
@@ -259,6 +340,7 @@ router.get('/', authenticate, async (req, res, next) => {
|
|||||||
return {
|
return {
|
||||||
id: match.id,
|
id: match.id,
|
||||||
slug: match.slug,
|
slug: match.slug,
|
||||||
|
type: 'manual',
|
||||||
partner: {
|
partner: {
|
||||||
id: partner.id,
|
id: partner.id,
|
||||||
username: partner.username,
|
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({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
count: transformedMatches.length,
|
count: allItems.length,
|
||||||
data: transformedMatches,
|
data: allItems,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
|
|||||||
@@ -47,6 +47,15 @@ const MatchCard = ({ match, onAccept, onReject, onOpenChat, processing }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<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 && (
|
{isIncoming && (
|
||||||
<span className="text-xs px-2 py-1 bg-amber-100 text-amber-700 rounded-full font-medium">
|
<span className="text-xs px-2 py-1 bg-amber-100 text-amber-700 rounded-full font-medium">
|
||||||
Incoming Request
|
Incoming Request
|
||||||
@@ -66,7 +75,7 @@ const MatchCard = ({ match, onAccept, onReject, onOpenChat, processing }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 ml-4">
|
<div className="flex items-center gap-2 ml-4">
|
||||||
{isIncoming && (
|
{isIncoming && match.type === 'manual' && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => onAccept(match.slug)}
|
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
|
<button
|
||||||
onClick={() => onReject(match.slug)}
|
onClick={() => onReject(match.slug)}
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
@@ -101,7 +119,16 @@ const MatchCard = ({ match, onAccept, onReject, onOpenChat, processing }) => {
|
|||||||
</button>
|
</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
|
<button
|
||||||
onClick={() => onOpenChat(match)}
|
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"
|
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
|
Open Chat
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isAccepted && !match.slug && (
|
||||||
|
<span className="text-sm text-gray-500 italic">
|
||||||
|
Chat not available yet
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user