feat(matching): add schedule config for division collision groups

Allow event organizers to configure which divisions run in parallel
(same time slot) for accurate collision detection in the auto-matching
algorithm. Divisions in the same slot will collide with each other.

- Add scheduleConfig JSON field to Event model
- Add PUT /events/:slug/schedule-config API endpoint
- Update matching algorithm to use slot-based collision detection
- Add UI in EventDetailsPage for managing division slots
- Add unit tests for schedule-based collision detection
This commit is contained in:
Radosław Gierwiało
2025-11-23 19:05:25 +01:00
parent a5a1296a4e
commit 4467c570b0
7 changed files with 526 additions and 17 deletions

View File

@@ -381,6 +381,7 @@ router.get('/:slug/details', authenticate, async (req, res, next) => {
description: event.description,
registrationDeadline: event.registrationDeadline,
matchingRunAt: event.matchingRunAt,
scheduleConfig: event.scheduleConfig,
},
checkin: {
token: checkinToken.token,
@@ -914,6 +915,76 @@ router.delete('/:slug/heats/:id', authenticate, async (req, res, next) => {
// AUTO-MATCHING ENDPOINTS
// ============================================
// PUT /api/events/:slug/schedule-config - Set schedule configuration (division order and collision groups)
router.put('/:slug/schedule-config', authenticate, async (req, res, next) => {
try {
const { slug } = req.params;
const { scheduleConfig } = req.body;
// Validate schedule config structure
if (scheduleConfig !== null && scheduleConfig !== undefined) {
if (!scheduleConfig.slots || !Array.isArray(scheduleConfig.slots)) {
return res.status(400).json({
success: false,
error: 'scheduleConfig must have a "slots" array',
});
}
// Validate each slot
for (let i = 0; i < scheduleConfig.slots.length; i++) {
const slot = scheduleConfig.slots[i];
if (typeof slot.order !== 'number' || !Array.isArray(slot.divisionIds)) {
return res.status(400).json({
success: false,
error: `Slot ${i} must have "order" (number) and "divisionIds" (array)`,
});
}
// Validate divisionIds are numbers
if (!slot.divisionIds.every(id => typeof id === 'number')) {
return res.status(400).json({
success: false,
error: `Slot ${i} divisionIds must be an array of numbers`,
});
}
}
}
// Find event
const event = await prisma.event.findUnique({
where: { slug },
select: { id: true },
});
if (!event) {
return res.status(404).json({
success: false,
error: 'Event not found',
});
}
// Update schedule config
const updated = await prisma.event.update({
where: { id: event.id },
data: {
scheduleConfig: scheduleConfig || null,
},
select: {
id: true,
slug: true,
scheduleConfig: true,
},
});
res.json({
success: true,
data: updated,
});
} catch (error) {
next(error);
}
});
// PUT /api/events/:slug/registration-deadline - Set registration deadline
router.put('/:slug/registration-deadline', authenticate, async (req, res, next) => {
try {