feat(dashboard): add Recording Assignments section

- Extend dashboard API to include recordingSuggestions for each event
- Add toBeRecorded and toRecord arrays with heat and user details
- Export RecordingSummaryCard component
- Add Recording Assignments section to DashboardPage
- Filter and display events with recording suggestions
- Show up to 2 suggestions per event with View Details link
This commit is contained in:
Radosław Gierwiało
2025-11-30 15:14:06 +01:00
parent 6ce3111cdd
commit d8799d03af
3 changed files with 93 additions and 1 deletions

View File

@@ -49,7 +49,7 @@ router.get('/', authenticate, async (req, res, next) => {
// Socket may not be initialized (e.g., during tests) // Socket may not be initialized (e.g., during tests)
} }
// Get user's heats for each event // Get user's heats and recording suggestions for each event
const activeEvents = await Promise.all( const activeEvents = await Promise.all(
eventParticipants.map(async (ep) => { eventParticipants.map(async (ep) => {
const heats = await prisma.eventUserHeat.findMany({ const heats = await prisma.eventUserHeat.findMany({
@@ -75,6 +75,47 @@ router.get('/', authenticate, async (req, res, next) => {
}, },
}); });
// 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 },
},
},
},
},
});
return { return {
id: ep.event.id, id: ep.event.id,
slug: ep.event.slug, slug: ep.event.slug,
@@ -91,6 +132,28 @@ router.get('/', authenticate, async (req, res, next) => {
heatNumber: h.heatNumber, heatNumber: h.heatNumber,
role: h.role, role: h.role,
})), })),
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,
})),
},
}; };
}) })
); );

View File

@@ -1,3 +1,4 @@
export { default as DashboardEventCard } from './DashboardEventCard'; export { default as DashboardEventCard } from './DashboardEventCard';
export { default as DashboardMatchCard } from './DashboardMatchCard'; export { default as DashboardMatchCard } from './DashboardMatchCard';
export { default as RecordingSummaryCard } from './RecordingSummaryCard';
export { IncomingRequestCard, OutgoingRequestCard } from './MatchRequestCards'; export { IncomingRequestCard, OutgoingRequestCard } from './MatchRequestCards';

View File

@@ -10,6 +10,7 @@ import EmptyState from '../components/common/EmptyState';
import { import {
DashboardEventCard, DashboardEventCard,
DashboardMatchCard, DashboardMatchCard,
RecordingSummaryCard,
IncomingRequestCard, IncomingRequestCard,
OutgoingRequestCard, OutgoingRequestCard,
} from '../components/dashboard'; } from '../components/dashboard';
@@ -20,6 +21,7 @@ import {
ChevronRight, ChevronRight,
Inbox, Inbox,
Send, Send,
Video,
} from 'lucide-react'; } from 'lucide-react';
const DashboardPage = () => { const DashboardPage = () => {
@@ -159,6 +161,14 @@ const DashboardPage = () => {
const hasIncoming = matchRequests?.incoming?.length > 0; const hasIncoming = matchRequests?.incoming?.length > 0;
const hasOutgoing = matchRequests?.outgoing?.length > 0; const hasOutgoing = matchRequests?.outgoing?.length > 0;
// Filter events with recording suggestions
const eventsWithRecordings = activeEvents?.filter(
(event) =>
event.recordingSuggestions &&
(event.recordingSuggestions.toBeRecorded.length > 0 ||
event.recordingSuggestions.toRecord.length > 0)
) || [];
return ( return (
<Layout pageTitle="Dashboard"> <Layout pageTitle="Dashboard">
<div className="max-w-5xl mx-auto"> <div className="max-w-5xl mx-auto">
@@ -207,6 +217,24 @@ const DashboardPage = () => {
)} )}
</section> </section>
{/* Recording Assignments Section */}
{eventsWithRecordings.length > 0 && (
<section className="mb-8">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-gray-900 flex items-center gap-2">
<Video className="w-5 h-5 text-primary-600" />
Recording Assignments
</h2>
</div>
<div className="grid gap-4 md:grid-cols-2">
{eventsWithRecordings.map((event) => (
<RecordingSummaryCard key={event.id} event={event} />
))}
</div>
</section>
)}
{/* Active Matches Section */} {/* Active Matches Section */}
<section className="mb-8"> <section className="mb-8">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">