🎪 Flujo de Organizador
Visión General
El flujo del ORGANIZER es el más completo en términos de gestión, incluyendo creación de hackathons, configuración, asignación de jueces y monitoreo.
Diagrama de Flujo Completo
Casos de Uso Detallados
CU-1: Crear Hackathon
Actor: ORGANIZER o ADMIN
Precondiciones: Usuario tiene role: ORGANIZER o ADMIN
Flujo Principal:
- ORGANIZER va a
/admin/hackathons/create - Completa formulario:
name(requerido)slug(único, para URL)description(requerido)- Fechas (7 fechas con orden estricto)
maxTeamSize,minTeamSize
- Sistema valida:
- Orden de fechas correcto
slugúnico
- Sistema crea
Hackathonconstatus: DRAFT - Redirige a
/admin/hackathons/[slug]
Validación de Fechas:
// Orden requerido:
registrationOpensAt < registrationClosesAt < startsAt <
submissionDeadline < judgingStartsAt < judgingEndsAt
CU-2: Configurar Criterios
Actor: ORGANIZER
Precondiciones: Hackathon existe en estado DRAFT
Flujo Principal:
- ORGANIZER va a
/admin/hackathons/[slug]/criteria - Ve lista de 12 criterios estándar
- Para cada criterio configura:
name(predefinido)description(opcional)weight(1-10, default 1)maxScore(default 10)
- Sistema crea registros
Criterion - Puede editar o eliminar criterios
12 Criterios Estándar:
- Stack Tecnológico
- Arquitectura del Sistema
- Características Principales
- Resolución de Desafíos Técnicos
- Visión y Mejoras Futuras
- Documentación Técnica
- Documentación de API
- Guía de Despliegue
- Cobertura de Tests
- Métricas de Rendimiento
- Repositorio y Código
- Demo Funcional
CU-3: Asignar Jueces
Actor: ORGANIZER
Precondiciones: Hackathon existe, hay usuarios con role: JUDGE
Flujo Principal:
- ORGANIZER va a
/admin/hackathons/[slug]/judges - Sistema muestra:
- Jueces ya asignados
- Jueces disponibles (filtrados)
- Selecciona jueces disponibles
- Sistema valida para cada juez:
- No está ya asignado
- No participa en el hackathon
- No es miembro de equipo
- Sistema crea
HackathonJudgepara cada juez - Jueces reciben notificación (email)
Filtrado de Jueces Disponibles:
export async function getAvailableJudges(hackathonId: string) {
// 1. Todos los jueces
const allJudges = await db.profile.findMany({
where: { role: 'JUDGE' },
});
// 2. Ya asignados
const assigned = await db.hackathonJudge.findMany({
where: { hackathonId },
});
// 3. Participantes
const participants = await db.hackathonParticipation.findMany({
where: { hackathonId },
});
// 4. Miembros de equipos
const teamMembers = await db.teamMember.findMany({
where: {
team: { hackathonId },
},
});
// Filtrar disponibles
return allJudges.filter(
(judge) =>
!assigned.some((a) => a.profileId === judge.id) &&
!participants.some((p) => p.profileId === judge.id) &&
!teamMembers.some((tm) => tm.profileId === judge.id)
);
}
CU-4: Publicar Hackathon
Actor: ORGANIZER
Precondiciones:
- Hackathon en estado DRAFT
- Tiene al menos 1 criterio
- Tiene al menos 1 juez asignado
now >= registrationOpensAt- Orden de fechas válido
Flujo Principal:
- ORGANIZER va a
/admin/hackathons/[slug] - Ve botón "Publicar Hackathon"
- Sistema valida precondiciones
- Sistema cambia
status: DRAFT → REGISTRATION - Hackathon ahora es visible para PARTICIPANTs
- Cron job puede cambiar estados automáticamente
Validación:
export async function publishHackathon(hackathonId: string) {
const user = await getCurrentUser();
requireRole(user, ['ORGANIZER', 'ADMIN']);
const hackathon = await db.hackathon.findUnique({
where: { id: hackathonId },
include: {
criteria: true,
judges: true,
},
});
if (hackathon.status !== 'DRAFT') {
throw new Error('Solo se pueden publicar hackathons en DRAFT');
}
if (hackathon.criteria.length === 0) {
throw new Error('Debe tener al menos un criterio');
}
if (hackathon.judges.length === 0) {
throw new Error('Debe tener al menos un juez asignado');
}
const now = new Date();
if (now < hackathon.registrationOpensAt) {
throw new Error('La fecha de apertura de registro no ha llegado');
}
await db.hackathon.update({
where: { id: hackathonId },
data: { status: 'REGISTRATION' },
});
}
CU-5: Extender Fechas
Actor: ORGANIZER
Precondiciones:
- Hackathon en estado REGISTRATION, RUNNING o JUDGING
- Solo puede extender fase SIGUIENTE
- Máximo +7 días
Flujo Principal:
- ORGANIZER va a
/admin/hackathons/[slug]/dates - Ve fechas actuales
- Selecciona fechas a extender (solo fase siguiente)
- Ingresa nuevas fechas
- Sistema valida:
- Solo fase siguiente
- Máximo +7 días
- Mantener orden de fechas
- Sistema actualiza fechas
- Mensaje de éxito
Reglas de Extensión:
- REGISTRATION: Puede extender
startsAt,submissionDeadline,judgingStartsAt,judgingEndsAt - RUNNING: Puede extender
submissionDeadline,judgingStartsAt,judgingEndsAt - JUDGING: Puede extender solo
judgingEndsAt
Validación:
export function validateDateExtension(
currentStatus: 'REGISTRATION' | 'RUNNING' | 'JUDGING',
dates: HackathonDates,
newDates: Partial<HackathonDates>
): void {
const allowedFields: (keyof HackathonDates)[] = [];
if (currentStatus === 'REGISTRATION') {
allowedFields.push('startsAt', 'submissionDeadline', 'judgingStartsAt', 'judgingEndsAt');
} else if (currentStatus === 'RUNNING') {
allowedFields.push('submissionDeadline', 'judgingStartsAt', 'judgingEndsAt');
} else if (currentStatus === 'JUDGING') {
allowedFields.push('judgingEndsAt');
}
// Verificar campos permitidos
for (const field of Object.keys(newDates) as (keyof HackathonDates)[]) {
if (!allowedFields.includes(field)) {
throw new Error(
`No se puede modificar ${field} cuando el hackathon está en estado ${currentStatus}`
);
}
}
// Verificar extensión máxima (+7 días)
const MAX_EXTENSION_DAYS = 7;
const MAX_EXTENSION_MS = MAX_EXTENSION_DAYS * 24 * 60 * 60 * 1000;
for (const field of allowedFields) {
if (newDates[field]) {
const diff = newDates[field]!.getTime() - dates[field].getTime();
if (diff < 0) {
throw new Error(`No se puede acortar ${field}`);
}
if (diff > MAX_EXTENSION_MS) {
throw new Error(
`La extensión de ${field} no puede exceder ${MAX_EXTENSION_DAYS} días`
);
}
}
}
// Validar orden de fechas
validateHackathonDates({ ...dates, ...newDates });
}
Cambios de Estado Automáticos
Cron Job (/api/cron/update-hackathon-states):
- Se ejecuta cada minuto
- Solo procesa hackathons publicados (no DRAFT)
- Cambia estados automáticamente según fechas
Monitoreo y Estadísticas
Estadísticas Disponibles
export async function getHackathonStats(hackathonId: string) {
const [participationsCount, teamsCount, submissionsCount] = await Promise.all([
db.hackathonParticipation.count({ where: { hackathonId } }),
db.team.count({ where: { hackathonId } }),
db.submission.count({ where: { hackathonId } }),
]);
// Calcular tamaño promedio de equipos
const teams = await db.team.findMany({
where: { hackathonId },
include: { _count: { select: { members: true } } },
});
const totalMembers = teams.reduce((sum, team) => sum + team._count.members, 0);
const averageTeamSize = teams.length > 0 ? totalMembers / teams.length : 0;
return {
totalParticipants: participationsCount,
totalTeams: teamsCount,
totalSubmissions: submissionsCount,
averageTeamSize: Math.round(averageTeamSize * 100) / 100,
};
}
Gestión de Participantes
Eliminar Participación
export async function removeParticipation(
hackathonId: string,
profileId: string
) {
const user = await getCurrentUser();
requireRole(user, ['ORGANIZER', 'ADMIN']);
// Verificar que el hackathon pertenece al organizador
const hackathon = await db.hackathon.findUnique({
where: { id: hackathonId },
});
if (hackathon.organizerId !== user.profile.id && user.profile.role !== 'ADMIN') {
throw new Error('No tienes permiso para gestionar este hackathon');
}
// Eliminar participación
await db.hackathonParticipation.delete({
where: {
hackathonId_profileId: {
hackathonId,
profileId,
},
},
});
// Si tiene equipo, eliminar de equipo
const teamMember = await db.teamMember.findFirst({
where: {
profileId,
team: { hackathonId },
},
});
if (teamMember) {
await db.teamMember.delete({
where: { id: teamMember.id },
});
// Verificar si equipo queda vacío
const team = await db.team.findUnique({
where: { id: teamMember.teamId },
include: {
_count: { select: { members: true, submissions: true } },
},
});
if (team._count.members === 0) {
if (team._count.submissions === 0) {
// Eliminar equipo si no tiene submission
await db.team.delete({ where: { id: team.id } });
} else {
// No se puede eliminar equipo con submission
throw new Error(
'No se puede eliminar el último miembro de un equipo con submission'
);
}
}
}
}
Próximos Pasos
- Sponsor Flow - Flujo del sponsor
- Hackathon Lifecycle - Ciclo de vida completo
- Team Formation - Lógica de equipos
Siguiente: Sponsor Flow