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:
@@ -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,
|
||||||
|
})),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user