Saltar al contenido principal

🎪 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:

  1. ORGANIZER va a /admin/hackathons/create
  2. Completa formulario:
    • name (requerido)
    • slug (único, para URL)
    • description (requerido)
    • Fechas (7 fechas con orden estricto)
    • maxTeamSize, minTeamSize
  3. Sistema valida:
    • Orden de fechas correcto
    • slug único
  4. Sistema crea Hackathon con status: DRAFT
  5. 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:

  1. ORGANIZER va a /admin/hackathons/[slug]/criteria
  2. Ve lista de 12 criterios estándar
  3. Para cada criterio configura:
    • name (predefinido)
    • description (opcional)
    • weight (1-10, default 1)
    • maxScore (default 10)
  4. Sistema crea registros Criterion
  5. Puede editar o eliminar criterios

12 Criterios Estándar:

  1. Stack Tecnológico
  2. Arquitectura del Sistema
  3. Características Principales
  4. Resolución de Desafíos Técnicos
  5. Visión y Mejoras Futuras
  6. Documentación Técnica
  7. Documentación de API
  8. Guía de Despliegue
  9. Cobertura de Tests
  10. Métricas de Rendimiento
  11. Repositorio y Código
  12. Demo Funcional

CU-3: Asignar Jueces

Actor: ORGANIZER
Precondiciones: Hackathon existe, hay usuarios con role: JUDGE

Flujo Principal:

  1. ORGANIZER va a /admin/hackathons/[slug]/judges
  2. Sistema muestra:
    • Jueces ya asignados
    • Jueces disponibles (filtrados)
  3. Selecciona jueces disponibles
  4. Sistema valida para cada juez:
    • No está ya asignado
    • No participa en el hackathon
    • No es miembro de equipo
  5. Sistema crea HackathonJudge para cada juez
  6. 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:

  1. ORGANIZER va a /admin/hackathons/[slug]
  2. Ve botón "Publicar Hackathon"
  3. Sistema valida precondiciones
  4. Sistema cambia status: DRAFT → REGISTRATION
  5. Hackathon ahora es visible para PARTICIPANTs
  6. 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:

  1. ORGANIZER va a /admin/hackathons/[slug]/dates
  2. Ve fechas actuales
  3. Selecciona fechas a extender (solo fase siguiente)
  4. Ingresa nuevas fechas
  5. Sistema valida:
    • Solo fase siguiente
    • Máximo +7 días
    • Mantener orden de fechas
  6. Sistema actualiza fechas
  7. 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


Siguiente: Sponsor Flow