many bug fixes and features added
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
using SportsDivision.Domain.Enums;
|
||||||
namespace SportsDivision.Application.DTOs;
|
namespace SportsDivision.Application.DTOs;
|
||||||
public class TournamentEventLevelDto { public int TournamentEventLevelId { get; set; } public int TournamentId { get; set; } public int EventId { get; set; } public string EventName { get; set; } = string.Empty; public int EventLevelId { get; set; } public string EventLevelName { get; set; } = string.Empty; public bool AgeRestrictionWaived { get; set; } public int RegistrationCount { get; set; } }
|
public class TournamentEventLevelDto { public int TournamentEventLevelId { get; set; } public int TournamentId { get; set; } public string TournamentName { get; set; } = string.Empty; public int EventId { get; set; } public string EventName { get; set; } = string.Empty; public EventCategory Category { get; set; } public int EventLevelId { get; set; } public string EventLevelName { get; set; } = string.Empty; public bool AgeRestrictionWaived { get; set; } public int RegistrationCount { get; set; } }
|
||||||
public class TournamentEventLevelCreateDto { public int TournamentId { get; set; } public int EventId { get; set; } public int EventLevelId { get; set; } public bool AgeRestrictionWaived { get; set; } }
|
public class TournamentEventLevelCreateDto { public int TournamentId { get; set; } public int EventId { get; set; } public int EventLevelId { get; set; } public bool AgeRestrictionWaived { get; set; } }
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
using SportsDivision.Application.DTOs;
|
using SportsDivision.Application.DTOs;
|
||||||
namespace SportsDivision.Application.Interfaces;
|
namespace SportsDivision.Application.Interfaces;
|
||||||
public interface IScoringService { int CalculatePoints(decimal rawPerformance, decimal a, decimal b, decimal c, bool isTrack); Task<ScoreDto> RecordScoreAsync(ScoreCreateDto dto, string recordedBy); Task CalculateFinalScoresAsync(int tournamentEventLevelId, string recordedBy); Task CalculatePlacementsAsync(int tournamentEventLevelId); Task<IEnumerable<SchoolPointsSummaryDto>> GetSchoolStandingsAsync(int tournamentId); Task<IEnumerable<ScoringConstantDto>> GetScoringConstantsAsync(); Task UpdateScoringConstantAsync(ScoringConstantUpdateDto dto); Task<IEnumerable<PlacementPointConfigDto>> GetPlacementPointConfigsAsync(); Task UpdatePlacementPointConfigAsync(PlacementPointConfigDto dto); }
|
public interface IScoringService { int CalculatePoints(decimal rawPerformance, decimal a, decimal b, decimal c, bool isTrack); Task<ScoreDto> RecordScoreAsync(ScoreCreateDto dto, string recordedBy); Task CalculateFinalScoresAsync(int tournamentEventLevelId, string recordedBy); Task CalculatePlacementsAsync(int tournamentEventLevelId); Task CalculateTrackFinalResultsAsync(int tournamentEventLevelId, string recordedBy); Task<IEnumerable<SchoolPointsSummaryDto>> GetSchoolStandingsAsync(int tournamentId); Task<IEnumerable<ScoringConstantDto>> GetScoringConstantsAsync(); Task UpdateScoringConstantAsync(ScoringConstantUpdateDto dto); Task<IEnumerable<PlacementPointConfigDto>> GetPlacementPointConfigsAsync(); Task UpdatePlacementPointConfigAsync(PlacementPointConfigDto dto); }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using SportsDivision.Application.DTOs;
|
using SportsDivision.Application.DTOs;
|
||||||
using SportsDivision.Domain.Enums;
|
using SportsDivision.Domain.Enums;
|
||||||
namespace SportsDivision.Application.Interfaces;
|
namespace SportsDivision.Application.Interfaces;
|
||||||
public interface ITournamentService { Task<IEnumerable<TournamentDto>> GetAllAsync(bool includeArchived = false); Task<TournamentDto?> GetByIdAsync(int id); Task<TournamentDto> CreateAsync(TournamentCreateDto dto); Task UpdateAsync(TournamentUpdateDto dto); Task UpdateStatusAsync(int id, TournamentStatus status); Task ArchiveAsync(int id); Task UnarchiveAsync(int id); Task DeleteAsync(int id); Task<IEnumerable<TournamentEventLevelDto>> GetEventLevelsAsync(int tournamentId); Task<TournamentEventLevelDto> AddEventLevelAsync(TournamentEventLevelCreateDto dto); Task RemoveEventLevelAsync(int tournamentEventLevelId); Task ToggleAgeWaiverAsync(int tournamentEventLevelId); }
|
public interface ITournamentService { Task<IEnumerable<TournamentDto>> GetAllAsync(bool includeArchived = false); Task<TournamentDto?> GetByIdAsync(int id); Task<TournamentDto> CreateAsync(TournamentCreateDto dto); Task UpdateAsync(TournamentUpdateDto dto); Task UpdateStatusAsync(int id, TournamentStatus status); Task ArchiveAsync(int id); Task UnarchiveAsync(int id); Task DeleteAsync(int id); Task<IEnumerable<TournamentEventLevelDto>> GetEventLevelsAsync(int tournamentId); Task<TournamentEventLevelDto?> GetEventLevelByIdAsync(int tournamentEventLevelId); Task<IEnumerable<EventLevelDto>> GetAllEventLevelsAsync(); Task<IEnumerable<TournamentEventLevelDto>> GetEventLevelsByCategoryAsync(EventCategory category); Task<TournamentEventLevelDto> AddEventLevelAsync(TournamentEventLevelCreateDto dto); Task RemoveEventLevelAsync(int tournamentEventLevelId); Task ToggleAgeWaiverAsync(int tournamentEventLevelId); }
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ public class MappingProfile : Profile
|
|||||||
CreateMap<TournamentUpdateDto, Tournament>();
|
CreateMap<TournamentUpdateDto, Tournament>();
|
||||||
|
|
||||||
CreateMap<TournamentEventLevel, TournamentEventLevelDto>()
|
CreateMap<TournamentEventLevel, TournamentEventLevelDto>()
|
||||||
|
.ForMember(d => d.TournamentName, o => o.MapFrom(s => s.Tournament != null ? s.Tournament.Name : string.Empty))
|
||||||
.ForMember(d => d.EventName, o => o.MapFrom(s => s.Event != null ? s.Event.Name : string.Empty))
|
.ForMember(d => d.EventName, o => o.MapFrom(s => s.Event != null ? s.Event.Name : string.Empty))
|
||||||
|
.ForMember(d => d.Category, o => o.MapFrom(s => s.Event != null ? s.Event.Category : default))
|
||||||
.ForMember(d => d.EventLevelName, o => o.MapFrom(s => s.EventLevel != null ? s.EventLevel.Name : string.Empty))
|
.ForMember(d => d.EventLevelName, o => o.MapFrom(s => s.EventLevel != null ? s.EventLevel.Name : string.Empty))
|
||||||
.ForMember(d => d.RegistrationCount, o => o.MapFrom(s => s.Registrations != null ? s.Registrations.Count : 0));
|
.ForMember(d => d.RegistrationCount, o => o.MapFrom(s => s.Registrations != null ? s.Registrations.Count : 0));
|
||||||
CreateMap<TournamentEventLevelCreateDto, TournamentEventLevel>();
|
CreateMap<TournamentEventLevelCreateDto, TournamentEventLevel>();
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public class EventService : IEventService
|
|||||||
|
|
||||||
public async Task<EventDto?> GetByIdAsync(int id)
|
public async Task<EventDto?> GetByIdAsync(int id)
|
||||||
{
|
{
|
||||||
var evt = await _uow.Events.GetByIdAsync(id);
|
var evt = await _uow.Events.GetWithEventLevelsAsync(id);
|
||||||
return evt == null ? null : _mapper.Map<EventDto>(evt);
|
return evt == null ? null : _mapper.Map<EventDto>(evt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -146,28 +146,32 @@ public class HighJumpService : IHighJumpService
|
|||||||
.ThenBy(r => r.TotalFails)
|
.ThenBy(r => r.TotalFails)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// Record scores
|
// Record scores: cleared height, placement, placement points, and World-Athletics points.
|
||||||
|
var placementPoints = (await _uow.PlacementPointConfigs.GetAllAsync())
|
||||||
|
.ToDictionary(p => p.Placement, p => p.Points);
|
||||||
|
var constant = await _uow.ScoringConstants.GetByEventAsync(tel.EventId);
|
||||||
|
|
||||||
for (int i = 0; i < sorted.Count; i++)
|
for (int i = 0; i < sorted.Count; i++)
|
||||||
{
|
{
|
||||||
|
int place = i + 1;
|
||||||
|
decimal cleared = sorted[i].HighestCleared;
|
||||||
|
int calc = (constant != null && cleared > constant.B)
|
||||||
|
? (int)Math.Floor((double)constant.A * Math.Pow((double)(cleared - constant.B), (double)constant.C))
|
||||||
|
: 0;
|
||||||
|
|
||||||
var score = await _uow.Scores.GetByRegistrationAsync(sorted[i].RegId);
|
var score = await _uow.Scores.GetByRegistrationAsync(sorted[i].RegId);
|
||||||
if (score == null)
|
var isNew = score == null;
|
||||||
{
|
score ??= new Score { EventRegistrationId = sorted[i].RegId };
|
||||||
score = new Score
|
|
||||||
{
|
score.RawPerformance = cleared;
|
||||||
EventRegistrationId = sorted[i].RegId,
|
score.CalculatedPoints = calc;
|
||||||
RawPerformance = sorted[i].HighestCleared,
|
score.Placement = place;
|
||||||
RecordedBy = recordedBy,
|
score.PlacementPoints = placementPoints.TryGetValue(place, out var pts) ? pts : 0;
|
||||||
RecordedAt = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
await _uow.Scores.AddAsync(score);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
score.RawPerformance = sorted[i].HighestCleared;
|
|
||||||
score.RecordedBy = recordedBy;
|
score.RecordedBy = recordedBy;
|
||||||
score.RecordedAt = DateTime.UtcNow;
|
score.RecordedAt = DateTime.UtcNow;
|
||||||
_uow.Scores.Update(score);
|
|
||||||
}
|
if (isNew) await _uow.Scores.AddAsync(score);
|
||||||
|
else _uow.Scores.Update(score);
|
||||||
}
|
}
|
||||||
await _uow.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public class SchoolService : ISchoolService
|
|||||||
|
|
||||||
public async Task<SchoolDto?> GetByIdAsync(int id)
|
public async Task<SchoolDto?> GetByIdAsync(int id)
|
||||||
{
|
{
|
||||||
var school = await _uow.Schools.GetByIdAsync(id);
|
var school = await _uow.Schools.GetWithStudentsAsync(id);
|
||||||
return school == null ? null : _mapper.Map<SchoolDto>(school);
|
return school == null ? null : _mapper.Map<SchoolDto>(school);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,62 @@ public class ScoringService : IScoringService
|
|||||||
await _uow.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task CalculateTrackFinalResultsAsync(int tournamentEventLevelId, string recordedBy)
|
||||||
|
{
|
||||||
|
var tel = await _uow.TournamentEventLevels.GetWithRegistrationsAsync(tournamentEventLevelId)
|
||||||
|
?? throw new KeyNotFoundException("Tournament event level not found.");
|
||||||
|
if (tel.Event.Category != Domain.Enums.EventCategory.Track)
|
||||||
|
throw new InvalidOperationException("Final standings can only be calculated for track events.");
|
||||||
|
|
||||||
|
var rounds = (await _uow.Rounds.GetByTournamentEventLevelAsync(tournamentEventLevelId)).ToList();
|
||||||
|
var finalRound = rounds.FirstOrDefault(r => r.RoundType == Domain.Enums.RoundType.Final)
|
||||||
|
?? rounds.OrderByDescending(r => r.RoundOrder).FirstOrDefault()
|
||||||
|
?? throw new InvalidOperationException("This event has no rounds yet — seed heats and record times first.");
|
||||||
|
|
||||||
|
// Finishers in the final = lanes with a recorded time (DNS/DNF/DQ excluded), fastest first.
|
||||||
|
var finishers = finalRound.Heats
|
||||||
|
.SelectMany(h => h.HeatLanes)
|
||||||
|
.Where(l => l.Time.HasValue && !l.IsDNS && !l.IsDNF && !l.IsDQ)
|
||||||
|
.OrderBy(l => l.Time)
|
||||||
|
.ToList();
|
||||||
|
if (finishers.Count == 0)
|
||||||
|
throw new InvalidOperationException("The final round has no recorded times yet.");
|
||||||
|
|
||||||
|
var constant = await _uow.ScoringConstants.GetByEventAsync(tel.EventId);
|
||||||
|
var placementPoints = (await _uow.PlacementPointConfigs.GetAllAsync())
|
||||||
|
.ToDictionary(p => p.Placement, p => p.Points);
|
||||||
|
var finalistRegIds = finishers.Select(f => f.EventRegistrationId).ToHashSet();
|
||||||
|
|
||||||
|
// Points come from the final only: drop scores for anyone not in the final.
|
||||||
|
var existing = (await _uow.Scores.GetByTournamentEventLevelAsync(tournamentEventLevelId)).ToList();
|
||||||
|
foreach (var sc in existing.Where(s => !finalistRegIds.Contains(s.EventRegistrationId)))
|
||||||
|
_uow.Scores.Remove(sc);
|
||||||
|
|
||||||
|
for (int i = 0; i < finishers.Count; i++)
|
||||||
|
{
|
||||||
|
var lane = finishers[i];
|
||||||
|
int place = i + 1;
|
||||||
|
int calc = constant != null
|
||||||
|
? CalculatePoints(lane.Time!.Value, constant.A, constant.B, constant.C, isTrack: true)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
var score = await _uow.Scores.GetByRegistrationAsync(lane.EventRegistrationId);
|
||||||
|
var isNew = score == null;
|
||||||
|
score ??= new Score { EventRegistrationId = lane.EventRegistrationId };
|
||||||
|
|
||||||
|
score.RawPerformance = lane.Time!.Value;
|
||||||
|
score.CalculatedPoints = calc;
|
||||||
|
score.Placement = place;
|
||||||
|
score.PlacementPoints = placementPoints.TryGetValue(place, out var pts) ? pts : 0;
|
||||||
|
score.RecordedBy = recordedBy;
|
||||||
|
score.RecordedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
if (isNew) await _uow.Scores.AddAsync(score);
|
||||||
|
else _uow.Scores.Update(score);
|
||||||
|
}
|
||||||
|
await _uow.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task CalculatePlacementsAsync(int tournamentEventLevelId)
|
public async Task CalculatePlacementsAsync(int tournamentEventLevelId)
|
||||||
{
|
{
|
||||||
var scores = (await _uow.Scores.GetByTournamentEventLevelAsync(tournamentEventLevelId))
|
var scores = (await _uow.Scores.GetByTournamentEventLevelAsync(tournamentEventLevelId))
|
||||||
|
|||||||
@@ -90,6 +90,35 @@ public class TournamentService : ITournamentService
|
|||||||
return _mapper.Map<IEnumerable<TournamentEventLevelDto>>(tels);
|
return _mapper.Map<IEnumerable<TournamentEventLevelDto>>(tels);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<TournamentEventLevelDto?> GetEventLevelByIdAsync(int tournamentEventLevelId)
|
||||||
|
{
|
||||||
|
var tel = await _uow.TournamentEventLevels.GetWithRegistrationsAsync(tournamentEventLevelId);
|
||||||
|
return tel == null ? null : _mapper.Map<TournamentEventLevelDto>(tel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<EventLevelDto>> GetAllEventLevelsAsync()
|
||||||
|
{
|
||||||
|
var levels = await _uow.EventLevels.GetAllAsync();
|
||||||
|
return _mapper.Map<IEnumerable<EventLevelDto>>(levels.OrderBy(l => l.SortOrder));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<TournamentEventLevelDto>> GetEventLevelsByCategoryAsync(EventCategory category)
|
||||||
|
{
|
||||||
|
var tournaments = await _uow.Tournaments.GetActiveAsync();
|
||||||
|
var result = new List<TournamentEventLevelDto>();
|
||||||
|
foreach (var t in tournaments)
|
||||||
|
{
|
||||||
|
var tels = await _uow.TournamentEventLevels.GetByTournamentAsync(t.TournamentId);
|
||||||
|
foreach (var tel in tels.Where(x => x.Event != null && x.Event.Category == category))
|
||||||
|
{
|
||||||
|
var dto = _mapper.Map<TournamentEventLevelDto>(tel);
|
||||||
|
dto.TournamentName = t.Name;
|
||||||
|
result.Add(dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<TournamentEventLevelDto> AddEventLevelAsync(TournamentEventLevelCreateDto dto)
|
public async Task<TournamentEventLevelDto> AddEventLevelAsync(TournamentEventLevelCreateDto dto)
|
||||||
{
|
{
|
||||||
var existing = await _uow.TournamentEventLevels.FindAsync(dto.TournamentId, dto.EventId, dto.EventLevelId);
|
var existing = await _uow.TournamentEventLevels.FindAsync(dto.TournamentId, dto.EventId, dto.EventLevelId);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace SportsDivision.Infrastructure.Repositories;
|
|||||||
public class EventRegistrationRepository : Repository<EventRegistration>, IEventRegistrationRepository
|
public class EventRegistrationRepository : Repository<EventRegistration>, IEventRegistrationRepository
|
||||||
{
|
{
|
||||||
public EventRegistrationRepository(ApplicationDbContext context) : base(context) { }
|
public EventRegistrationRepository(ApplicationDbContext context) : base(context) { }
|
||||||
public async Task<IEnumerable<EventRegistration>> GetByTournamentEventLevelAsync(int tournamentEventLevelId) => await _dbSet.Where(r => r.TournamentEventLevelId == tournamentEventLevelId).Include(r => r.Student).ThenInclude(s => s!.School).Include(r => r.Score).ToListAsync();
|
public async Task<IEnumerable<EventRegistration>> GetByTournamentEventLevelAsync(int tournamentEventLevelId) => await _dbSet.Where(r => r.TournamentEventLevelId == tournamentEventLevelId).Include(r => r.Student).ThenInclude(s => s!.School).Include(r => r.TournamentEventLevel).ThenInclude(t => t.Event).Include(r => r.TournamentEventLevel).ThenInclude(t => t.EventLevel).Include(r => r.Score).ToListAsync();
|
||||||
public async Task<IEnumerable<EventRegistration>> GetByStudentAsync(int studentId) => await _dbSet.Where(r => r.StudentId == studentId).Include(r => r.TournamentEventLevel).ThenInclude(t => t.Event).Include(r => r.TournamentEventLevel).ThenInclude(t => t.EventLevel).Include(r => r.TournamentEventLevel).ThenInclude(t => t.Tournament).Include(r => r.Score).ToListAsync();
|
public async Task<IEnumerable<EventRegistration>> GetByStudentAsync(int studentId) => await _dbSet.Where(r => r.StudentId == studentId).Include(r => r.TournamentEventLevel).ThenInclude(t => t.Event).Include(r => r.TournamentEventLevel).ThenInclude(t => t.EventLevel).Include(r => r.TournamentEventLevel).ThenInclude(t => t.Tournament).Include(r => r.Score).ToListAsync();
|
||||||
public async Task<bool> IsStudentRegisteredAsync(int tournamentEventLevelId, int studentId) => await _dbSet.AnyAsync(r => r.TournamentEventLevelId == tournamentEventLevelId && r.StudentId == studentId);
|
public async Task<bool> IsStudentRegisteredAsync(int tournamentEventLevelId, int studentId) => await _dbSet.AnyAsync(r => r.TournamentEventLevelId == tournamentEventLevelId && r.StudentId == studentId);
|
||||||
public async Task<IEnumerable<EventRegistration>> GetBySchoolAndTournamentAsync(int schoolId, int tournamentId) => await _dbSet.Where(r => r.Student != null && r.Student.SchoolId == schoolId && r.TournamentEventLevel.TournamentId == tournamentId).Include(r => r.Student).Include(r => r.TournamentEventLevel).ThenInclude(t => t.Event).Include(r => r.TournamentEventLevel).ThenInclude(t => t.EventLevel).Include(r => r.Score).ToListAsync();
|
public async Task<IEnumerable<EventRegistration>> GetBySchoolAndTournamentAsync(int schoolId, int tournamentId) => await _dbSet.Where(r => r.Student != null && r.Student.SchoolId == schoolId && r.TournamentEventLevel.TournamentId == tournamentId).Include(r => r.Student).Include(r => r.TournamentEventLevel).ThenInclude(t => t.Event).Include(r => r.TournamentEventLevel).ThenInclude(t => t.EventLevel).Include(r => r.Score).ToListAsync();
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public class Repository<T> : IRepository<T> where T : class
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<T?> GetByIdAsync(int id) => await _dbSet.FindAsync(id);
|
public async Task<T?> GetByIdAsync(int id) => await _dbSet.FindAsync(id);
|
||||||
public async Task<IEnumerable<T>> GetAllAsync() => await _dbSet.ToListAsync();
|
public virtual async Task<IEnumerable<T>> GetAllAsync() => await _dbSet.ToListAsync();
|
||||||
public async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate) => await _dbSet.Where(predicate).ToListAsync();
|
public async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate) => await _dbSet.Where(predicate).ToListAsync();
|
||||||
public async Task<T> AddAsync(T entity) { await _dbSet.AddAsync(entity); return entity; }
|
public async Task<T> AddAsync(T entity) { await _dbSet.AddAsync(entity); return entity; }
|
||||||
public async Task AddRangeAsync(IEnumerable<T> entities) => await _dbSet.AddRangeAsync(entities);
|
public async Task AddRangeAsync(IEnumerable<T> entities) => await _dbSet.AddRangeAsync(entities);
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ namespace SportsDivision.Infrastructure.Repositories;
|
|||||||
public class RoundRepository : Repository<Round>, IRoundRepository
|
public class RoundRepository : Repository<Round>, IRoundRepository
|
||||||
{
|
{
|
||||||
public RoundRepository(ApplicationDbContext context) : base(context) { }
|
public RoundRepository(ApplicationDbContext context) : base(context) { }
|
||||||
public async Task<IEnumerable<Round>> GetByTournamentEventLevelAsync(int tournamentEventLevelId) => await _dbSet.Where(r => r.TournamentEventLevelId == tournamentEventLevelId).Include(r => r.Heats).OrderBy(r => r.RoundOrder).ToListAsync();
|
public async Task<IEnumerable<Round>> GetByTournamentEventLevelAsync(int tournamentEventLevelId) => await _dbSet.Where(r => r.TournamentEventLevelId == tournamentEventLevelId).Include(r => r.Heats).ThenInclude(h => h.HeatLanes).OrderBy(r => r.RoundOrder).ToListAsync();
|
||||||
public async Task<Round?> GetWithHeatsAsync(int roundId) => await _dbSet.Include(r => r.Heats).ThenInclude(h => h.HeatLanes).ThenInclude(hl => hl.EventRegistration).ThenInclude(er => er.Student).ThenInclude(s => s!.School).Include(r => r.TournamentEventLevel).ThenInclude(t => t.Event).Include(r => r.TournamentEventLevel).ThenInclude(t => t.EventLevel).FirstOrDefaultAsync(r => r.RoundId == roundId);
|
public async Task<Round?> GetWithHeatsAsync(int roundId) => await _dbSet.Include(r => r.Heats).ThenInclude(h => h.HeatLanes).ThenInclude(hl => hl.EventRegistration).ThenInclude(er => er.Student).ThenInclude(s => s!.School).Include(r => r.TournamentEventLevel).ThenInclude(t => t.Event).Include(r => r.TournamentEventLevel).ThenInclude(t => t.EventLevel).FirstOrDefaultAsync(r => r.RoundId == roundId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ namespace SportsDivision.Infrastructure.Repositories;
|
|||||||
public class SchoolRepository : Repository<School>, ISchoolRepository
|
public class SchoolRepository : Repository<School>, ISchoolRepository
|
||||||
{
|
{
|
||||||
public SchoolRepository(ApplicationDbContext context) : base(context) { }
|
public SchoolRepository(ApplicationDbContext context) : base(context) { }
|
||||||
public async Task<IEnumerable<School>> GetByZoneAsync(int zoneId) => await _dbSet.Where(s => s.ZoneId == zoneId).Include(s => s.Zone).OrderBy(s => s.Name).ToListAsync();
|
public override async Task<IEnumerable<School>> GetAllAsync() => await _dbSet.Include(s => s.Zone).Include(s => s.Students).OrderBy(s => s.Name).ToListAsync();
|
||||||
public async Task<IEnumerable<School>> GetBySchoolLevelAsync(SchoolLevel level) => await _dbSet.Where(s => s.SchoolLevel == level).Include(s => s.Zone).OrderBy(s => s.Name).ToListAsync();
|
public async Task<IEnumerable<School>> GetByZoneAsync(int zoneId) => await _dbSet.Where(s => s.ZoneId == zoneId).Include(s => s.Zone).Include(s => s.Students).OrderBy(s => s.Name).ToListAsync();
|
||||||
|
public async Task<IEnumerable<School>> GetBySchoolLevelAsync(SchoolLevel level) => await _dbSet.Where(s => s.SchoolLevel == level).Include(s => s.Zone).Include(s => s.Students).OrderBy(s => s.Name).ToListAsync();
|
||||||
public async Task<School?> GetWithStudentsAsync(int schoolId) => await _dbSet.Include(s => s.Students).Include(s => s.Zone).FirstOrDefaultAsync(s => s.SchoolId == schoolId);
|
public async Task<School?> GetWithStudentsAsync(int schoolId) => await _dbSet.Include(s => s.Students).Include(s => s.Zone).FirstOrDefaultAsync(s => s.SchoolId == schoolId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ namespace SportsDivision.Infrastructure.Repositories;
|
|||||||
public class ScoringConstantRepository : Repository<ScoringConstant>, IScoringConstantRepository
|
public class ScoringConstantRepository : Repository<ScoringConstant>, IScoringConstantRepository
|
||||||
{
|
{
|
||||||
public ScoringConstantRepository(ApplicationDbContext context) : base(context) { }
|
public ScoringConstantRepository(ApplicationDbContext context) : base(context) { }
|
||||||
|
public override async Task<IEnumerable<ScoringConstant>> GetAllAsync() => await _dbSet.Include(s => s.Event).OrderBy(s => s.Event!.Name).ToListAsync();
|
||||||
public async Task<ScoringConstant?> GetByEventAsync(int eventId) => await _dbSet.FirstOrDefaultAsync(s => s.EventId == eventId);
|
public async Task<ScoringConstant?> GetByEventAsync(int eventId) => await _dbSet.FirstOrDefaultAsync(s => s.EventId == eventId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ namespace SportsDivision.Infrastructure.Repositories;
|
|||||||
public class TournamentRepository : Repository<Tournament>, ITournamentRepository
|
public class TournamentRepository : Repository<Tournament>, ITournamentRepository
|
||||||
{
|
{
|
||||||
public TournamentRepository(ApplicationDbContext context) : base(context) { }
|
public TournamentRepository(ApplicationDbContext context) : base(context) { }
|
||||||
public async Task<IEnumerable<Tournament>> GetActiveAsync() => await _dbSet.Where(t => !t.IsArchived).Include(t => t.Zone).OrderByDescending(t => t.StartDate).ToListAsync();
|
public override async Task<IEnumerable<Tournament>> GetAllAsync() => await _dbSet.Include(t => t.Zone).Include(t => t.TournamentEventLevels).OrderByDescending(t => t.StartDate).ToListAsync();
|
||||||
|
public async Task<IEnumerable<Tournament>> GetActiveAsync() => await _dbSet.Where(t => !t.IsArchived).Include(t => t.Zone).Include(t => t.TournamentEventLevels).OrderByDescending(t => t.StartDate).ToListAsync();
|
||||||
public async Task<Tournament?> GetWithEventLevelsAsync(int tournamentId) => await _dbSet.Include(t => t.TournamentEventLevels).ThenInclude(te => te.Event).Include(t => t.TournamentEventLevels).ThenInclude(te => te.EventLevel).Include(t => t.Zone).FirstOrDefaultAsync(t => t.TournamentId == tournamentId);
|
public async Task<Tournament?> GetWithEventLevelsAsync(int tournamentId) => await _dbSet.Include(t => t.TournamentEventLevels).ThenInclude(te => te.Event).Include(t => t.TournamentEventLevels).ThenInclude(te => te.EventLevel).Include(t => t.Zone).FirstOrDefaultAsync(t => t.TournamentId == tournamentId);
|
||||||
public async Task<Tournament?> GetFullTournamentAsync(int tournamentId) => await _dbSet.Include(t => t.Zone).Include(t => t.TournamentEventLevels).ThenInclude(te => te.Event).Include(t => t.TournamentEventLevels).ThenInclude(te => te.EventLevel).Include(t => t.TournamentEventLevels).ThenInclude(te => te.Registrations).ThenInclude(r => r.Student).ThenInclude(s => s!.School).FirstOrDefaultAsync(t => t.TournamentId == tournamentId);
|
public async Task<Tournament?> GetFullTournamentAsync(int tournamentId) => await _dbSet.Include(t => t.Zone).Include(t => t.TournamentEventLevels).ThenInclude(te => te.Event).Include(t => t.TournamentEventLevels).ThenInclude(te => te.EventLevel).Include(t => t.TournamentEventLevels).ThenInclude(te => te.Registrations).ThenInclude(r => r.Student).ThenInclude(s => s!.School).FirstOrDefaultAsync(t => t.TournamentId == tournamentId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,9 +248,9 @@ public class DatabaseSeeder
|
|||||||
Add("400m", 1.53775m, 82m, 1.81m, "seconds");
|
Add("400m", 1.53775m, 82m, 1.81m, "seconds");
|
||||||
Add("1500m", 0.03768m, 480m, 1.85m, "seconds");
|
Add("1500m", 0.03768m, 480m, 1.85m, "seconds");
|
||||||
Add("80mH", 5.74352m, 28.5m, 1.92m, "seconds");
|
Add("80mH", 5.74352m, 28.5m, 1.92m, "seconds");
|
||||||
Add("Long Jump", 0.14354m, 220m, 1.4m, "centimetres");
|
Add("Long Jump", 90.56762m, 2.2m, 1.4m, "metres");
|
||||||
Add("Triple Jump", 0.14354m, 220m, 1.4m, "centimetres");
|
Add("Triple Jump", 90.56762m, 2.2m, 1.4m, "metres");
|
||||||
Add("High Jump", 0.8465m, 75m, 1.42m, "centimetres");
|
Add("High Jump", 585.63492m, 0.75m, 1.42m, "metres");
|
||||||
Add("Shot Put 3kg", 51.39m, 1.5m, 1.05m, "metres"); Add("Shot Put 4kg", 51.39m, 1.5m, 1.05m, "metres");
|
Add("Shot Put 3kg", 51.39m, 1.5m, 1.05m, "metres"); Add("Shot Put 4kg", 51.39m, 1.5m, 1.05m, "metres");
|
||||||
Add("Shot Put 5kg", 51.39m, 1.5m, 1.05m, "metres"); Add("Shot Put 6kg", 51.39m, 1.5m, 1.05m, "metres");
|
Add("Shot Put 5kg", 51.39m, 1.5m, 1.05m, "metres"); Add("Shot Put 6kg", 51.39m, 1.5m, 1.05m, "metres");
|
||||||
Add("Discus 1kg", 12.91m, 4m, 1.1m, "metres"); Add("Discus 1.25kg", 12.91m, 4m, 1.1m, "metres");
|
Add("Discus 1kg", 12.91m, 4m, 1.1m, "metres"); Add("Discus 1.25kg", 12.91m, 4m, 1.1m, "metres");
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using SportsDivision.Application.DTOs;
|
using SportsDivision.Application.DTOs;
|
||||||
using SportsDivision.Application.Interfaces;
|
using SportsDivision.Application.Interfaces;
|
||||||
using SportsDivision.Domain.Enums;
|
using SportsDivision.Domain.Enums;
|
||||||
|
using SportsDivision.Web.Helpers;
|
||||||
|
|
||||||
namespace SportsDivision.Web.Controllers;
|
namespace SportsDivision.Web.Controllers;
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ public class EventController : Controller
|
|||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> Index(EventCategory? category)
|
public async Task<IActionResult> Index(EventCategory? category, int page = 1, int pageSize = PaginationHelper.PageSize)
|
||||||
{
|
{
|
||||||
IEnumerable<EventDto> events;
|
IEnumerable<EventDto> events;
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ public class EventController : Controller
|
|||||||
|
|
||||||
ViewBag.SelectedCategory = category;
|
ViewBag.SelectedCategory = category;
|
||||||
|
|
||||||
return View(events);
|
return View(this.Page(events, page, pageSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using SportsDivision.Application.DTOs;
|
using SportsDivision.Application.DTOs;
|
||||||
using SportsDivision.Application.Interfaces;
|
using SportsDivision.Application.Interfaces;
|
||||||
|
using SportsDivision.Domain.Enums;
|
||||||
|
|
||||||
namespace SportsDivision.Web.Controllers;
|
namespace SportsDivision.Web.Controllers;
|
||||||
|
|
||||||
@@ -25,17 +26,26 @@ public class FieldEventController : Controller
|
|||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> Index(int tournamentEventLevelId)
|
public async Task<IActionResult> Index(int tournamentEventLevelId)
|
||||||
{
|
{
|
||||||
|
if (tournamentEventLevelId <= 0)
|
||||||
|
{
|
||||||
|
ViewBag.ScoringTargets = await _tournamentService.GetEventLevelsByCategoryAsync(EventCategory.Field);
|
||||||
|
ViewBag.ScoringController = "FieldEvent";
|
||||||
|
ViewBag.ScoringTitle = "Field Event Scoring";
|
||||||
|
return View("SelectEventLevel");
|
||||||
|
}
|
||||||
var registrations = await _registrationService.GetByTournamentEventLevelAsync(tournamentEventLevelId);
|
var registrations = await _registrationService.GetByTournamentEventLevelAsync(tournamentEventLevelId);
|
||||||
ViewBag.TournamentEventLevelId = tournamentEventLevelId;
|
ViewBag.TournamentEventLevelId = tournamentEventLevelId;
|
||||||
return View(registrations);
|
return View(registrations);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> RecordScore([FromBody] ScoreCreateDto dto)
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> RecordScore(ScoreCreateDto dto, int tournamentEventLevelId)
|
||||||
{
|
{
|
||||||
var recordedBy = User.Identity?.Name ?? "Unknown";
|
var recordedBy = User.Identity?.Name ?? "Unknown";
|
||||||
var score = await _scoringService.RecordScoreAsync(dto, recordedBy);
|
await _scoringService.RecordScoreAsync(dto, recordedBy);
|
||||||
return Json(new { success = true, score });
|
TempData["SuccessMessage"] = "Performance recorded.";
|
||||||
|
return RedirectToAction(nameof(Index), new { tournamentEventLevelId });
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using SportsDivision.Application.DTOs;
|
using SportsDivision.Application.DTOs;
|
||||||
using SportsDivision.Application.Interfaces;
|
using SportsDivision.Application.Interfaces;
|
||||||
|
using SportsDivision.Domain.Enums;
|
||||||
|
|
||||||
namespace SportsDivision.Web.Controllers;
|
namespace SportsDivision.Web.Controllers;
|
||||||
|
|
||||||
@@ -28,6 +29,13 @@ public class HighJumpController : Controller
|
|||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> Index(int tournamentEventLevelId)
|
public async Task<IActionResult> Index(int tournamentEventLevelId)
|
||||||
{
|
{
|
||||||
|
if (tournamentEventLevelId <= 0)
|
||||||
|
{
|
||||||
|
ViewBag.ScoringTargets = await _tournamentService.GetEventLevelsByCategoryAsync(EventCategory.HighJump);
|
||||||
|
ViewBag.ScoringController = "HighJump";
|
||||||
|
ViewBag.ScoringTitle = "High Jump Scoring";
|
||||||
|
return View("SelectEventLevel");
|
||||||
|
}
|
||||||
var heights = await _highJumpService.GetHeightsAsync(tournamentEventLevelId);
|
var heights = await _highJumpService.GetHeightsAsync(tournamentEventLevelId);
|
||||||
var registrations = await _registrationService.GetByTournamentEventLevelAsync(tournamentEventLevelId);
|
var registrations = await _registrationService.GetByTournamentEventLevelAsync(tournamentEventLevelId);
|
||||||
ViewBag.TournamentEventLevelId = tournamentEventLevelId;
|
ViewBag.TournamentEventLevelId = tournamentEventLevelId;
|
||||||
@@ -57,12 +65,11 @@ public class HighJumpController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> RecordAttempt([FromBody] HighJumpAttemptUpdateDto dto)
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> RecordAttempt(HighJumpAttemptUpdateDto dto, int tournamentEventLevelId)
|
||||||
{
|
{
|
||||||
await _highJumpService.RecordAttemptAsync(dto);
|
await _highJumpService.RecordAttemptAsync(dto);
|
||||||
var isEliminated = await _highJumpService.IsEliminatedAsync(
|
return RedirectToAction(nameof(Index), new { tournamentEventLevelId });
|
||||||
dto.HighJumpHeightId, dto.EventRegistrationId);
|
|
||||||
return Json(new { success = true, isEliminated });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public class ReportController : Controller
|
|||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index()
|
||||||
{
|
{
|
||||||
var tournaments = await _tournamentService.GetAllAsync();
|
var tournaments = await _tournamentService.GetAllAsync();
|
||||||
|
ViewBag.Tournaments = tournaments;
|
||||||
return View(tournaments);
|
return View(tournaments);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,4 +155,65 @@ public class ReportController : Controller
|
|||||||
var pdf = document.GeneratePdf();
|
var pdf = document.GeneratePdf();
|
||||||
return File(pdf, "application/pdf", $"StudentPoints_{tournamentId}.pdf");
|
return File(pdf, "application/pdf", $"StudentPoints_{tournamentId}.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----- Excel (.xlsx) exports -----
|
||||||
|
|
||||||
|
private async Task<string> TournamentNameAsync(int tournamentId)
|
||||||
|
=> (await _tournamentService.GetByIdAsync(tournamentId))?.Name ?? "Tournament";
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> PopularEventsExcel(int tournamentId)
|
||||||
|
{
|
||||||
|
var report = await _reportService.GetPopularEventsAsync(tournamentId);
|
||||||
|
var bytes = ReportExcelExporter.PopularEvents(report, await TournamentNameAsync(tournamentId));
|
||||||
|
return File(bytes, ReportExcelExporter.MimeType, $"PopularEvents_{tournamentId}.xlsx");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> RegistrationByGenderExcel(int tournamentId, int? zoneId)
|
||||||
|
{
|
||||||
|
var report = await _reportService.GetRegistrationByGenderAsync(tournamentId, zoneId);
|
||||||
|
var bytes = ReportExcelExporter.RegistrationByGender(report, await TournamentNameAsync(tournamentId));
|
||||||
|
return File(bytes, ReportExcelExporter.MimeType, $"RegistrationByGender_{tournamentId}.xlsx");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> EventSchoolReportExcel(int tournamentId, int? eventId, int? eventLevelId)
|
||||||
|
{
|
||||||
|
var report = await _reportService.GetEventSchoolReportAsync(tournamentId, eventId, eventLevelId);
|
||||||
|
var bytes = ReportExcelExporter.EventSchoolReport(report, await TournamentNameAsync(tournamentId));
|
||||||
|
return File(bytes, ReportExcelExporter.MimeType, $"EventSchoolReport_{tournamentId}.xlsx");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> StudentsBySchoolExcel(int tournamentId, int? schoolId, int? zoneId)
|
||||||
|
{
|
||||||
|
var report = await _reportService.GetStudentsBySchoolAsync(tournamentId, schoolId, zoneId);
|
||||||
|
var bytes = ReportExcelExporter.StudentsBySchool(report, await TournamentNameAsync(tournamentId));
|
||||||
|
return File(bytes, ReportExcelExporter.MimeType, $"StudentsBySchool_{tournamentId}.xlsx");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> ScoresByEventExcel(int tournamentId, int? eventId, int? eventLevelId)
|
||||||
|
{
|
||||||
|
var report = await _reportService.GetScoresByEventAsync(tournamentId, eventId, eventLevelId);
|
||||||
|
var bytes = ReportExcelExporter.ScoresByEvent(report, await TournamentNameAsync(tournamentId));
|
||||||
|
return File(bytes, ReportExcelExporter.MimeType, $"ScoresByEvent_{tournamentId}.xlsx");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> StudentPointsExcel(int tournamentId, int? schoolId, int? zoneId)
|
||||||
|
{
|
||||||
|
var report = await _reportService.GetStudentPointsAsync(tournamentId, schoolId, zoneId);
|
||||||
|
var bytes = ReportExcelExporter.StudentPoints(report, await TournamentNameAsync(tournamentId));
|
||||||
|
return File(bytes, ReportExcelExporter.MimeType, $"StudentPoints_{tournamentId}.xlsx");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> SchoolStandingsExcel(int tournamentId)
|
||||||
|
{
|
||||||
|
var standings = await _scoringService.GetSchoolStandingsAsync(tournamentId);
|
||||||
|
var bytes = ReportExcelExporter.SchoolStandings(standings, await TournamentNameAsync(tournamentId));
|
||||||
|
return File(bytes, ReportExcelExporter.MimeType, $"SchoolStandings_{tournamentId}.xlsx");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using SportsDivision.Application.DTOs;
|
|||||||
using SportsDivision.Application.Interfaces;
|
using SportsDivision.Application.Interfaces;
|
||||||
using SportsDivision.Domain.Enums;
|
using SportsDivision.Domain.Enums;
|
||||||
using SportsDivision.Domain.Interfaces;
|
using SportsDivision.Domain.Interfaces;
|
||||||
|
using SportsDivision.Web.Helpers;
|
||||||
|
|
||||||
namespace SportsDivision.Web.Controllers;
|
namespace SportsDivision.Web.Controllers;
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ public class SchoolController : Controller
|
|||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> Index(int? zoneId, SchoolLevel? level)
|
public async Task<IActionResult> Index(int? zoneId, SchoolLevel? level, int page = 1, int pageSize = PaginationHelper.PageSize)
|
||||||
{
|
{
|
||||||
IEnumerable<SchoolDto> schools;
|
IEnumerable<SchoolDto> schools;
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ public class SchoolController : Controller
|
|||||||
ViewBag.SelectedZoneId = zoneId;
|
ViewBag.SelectedZoneId = zoneId;
|
||||||
ViewBag.SelectedLevel = level;
|
ViewBag.SelectedLevel = level;
|
||||||
|
|
||||||
return View(schools);
|
return View(this.Page(schools, page, pageSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> Details(int id)
|
public async Task<IActionResult> Details(int id)
|
||||||
@@ -174,6 +175,12 @@ public class SchoolController : Controller
|
|||||||
private async Task PopulateZonesViewBag()
|
private async Task PopulateZonesViewBag()
|
||||||
{
|
{
|
||||||
var zones = await _unitOfWork.Zones.GetAllAsync();
|
var zones = await _unitOfWork.Zones.GetAllAsync();
|
||||||
ViewBag.Zones = zones;
|
ViewBag.Zones = zones.Select(z => new ZoneDto
|
||||||
|
{
|
||||||
|
ZoneId = z.ZoneId,
|
||||||
|
Name = z.Name,
|
||||||
|
Code = z.Code,
|
||||||
|
IsActive = z.IsActive
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public class ScoringConfigController : Controller
|
|||||||
{
|
{
|
||||||
var constants = await _scoringService.GetScoringConstantsAsync();
|
var constants = await _scoringService.GetScoringConstantsAsync();
|
||||||
var placementConfigs = await _scoringService.GetPlacementPointConfigsAsync();
|
var placementConfigs = await _scoringService.GetPlacementPointConfigsAsync();
|
||||||
|
ViewBag.ScoringConstants = constants;
|
||||||
ViewBag.PlacementPointConfigs = placementConfigs;
|
ViewBag.PlacementPointConfigs = placementConfigs;
|
||||||
return View(constants);
|
return View(constants);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using SportsDivision.Application.DTOs;
|
using SportsDivision.Application.DTOs;
|
||||||
using SportsDivision.Application.Interfaces;
|
using SportsDivision.Application.Interfaces;
|
||||||
|
using SportsDivision.Web.Helpers;
|
||||||
|
|
||||||
namespace SportsDivision.Web.Controllers;
|
namespace SportsDivision.Web.Controllers;
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ public class StudentController : Controller
|
|||||||
_schoolService = schoolService;
|
_schoolService = schoolService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> Index(int? schoolId, string? search)
|
public async Task<IActionResult> Index(int? schoolId, string? search, int page = 1, int pageSize = PaginationHelper.PageSize)
|
||||||
{
|
{
|
||||||
IEnumerable<StudentDto> students;
|
IEnumerable<StudentDto> students;
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ public class StudentController : Controller
|
|||||||
ViewBag.SelectedSchoolId = schoolId;
|
ViewBag.SelectedSchoolId = schoolId;
|
||||||
ViewBag.SearchTerm = search;
|
ViewBag.SearchTerm = search;
|
||||||
|
|
||||||
return View(students);
|
return View(this.Page(students, page, pageSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> Details(int id)
|
public async Task<IActionResult> Details(int id)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using SportsDivision.Application.DTOs;
|
using SportsDivision.Application.DTOs;
|
||||||
using SportsDivision.Application.Interfaces;
|
using SportsDivision.Application.Interfaces;
|
||||||
using SportsDivision.Domain.Enums;
|
using SportsDivision.Domain.Enums;
|
||||||
|
using SportsDivision.Web.Helpers;
|
||||||
|
|
||||||
namespace SportsDivision.Web.Controllers;
|
namespace SportsDivision.Web.Controllers;
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ public class TournamentController : Controller
|
|||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> Index(TournamentStatus? status, bool showArchived = false)
|
public async Task<IActionResult> Index(TournamentStatus? status, bool showArchived = false, int page = 1, int pageSize = PaginationHelper.PageSize)
|
||||||
{
|
{
|
||||||
var tournaments = await _tournamentService.GetAllAsync(includeArchived: showArchived);
|
var tournaments = await _tournamentService.GetAllAsync(includeArchived: showArchived);
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ public class TournamentController : Controller
|
|||||||
ViewBag.SelectedStatus = status;
|
ViewBag.SelectedStatus = status;
|
||||||
ViewBag.ShowArchived = showArchived;
|
ViewBag.ShowArchived = showArchived;
|
||||||
|
|
||||||
return View(tournaments);
|
return View(this.Page(tournaments, page, pageSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> Details(int id)
|
public async Task<IActionResult> Details(int id)
|
||||||
@@ -45,6 +46,8 @@ public class TournamentController : Controller
|
|||||||
|
|
||||||
var eventLevels = await _tournamentService.GetEventLevelsAsync(id);
|
var eventLevels = await _tournamentService.GetEventLevelsAsync(id);
|
||||||
ViewBag.EventLevels = eventLevels;
|
ViewBag.EventLevels = eventLevels;
|
||||||
|
ViewBag.Events = await _eventService.GetAllAsync();
|
||||||
|
ViewBag.Levels = await _tournamentService.GetAllEventLevelsAsync();
|
||||||
|
|
||||||
return View(tournament);
|
return View(tournament);
|
||||||
}
|
}
|
||||||
@@ -158,32 +161,6 @@ public class TournamentController : Controller
|
|||||||
return RedirectToAction(nameof(Index));
|
return RedirectToAction(nameof(Index));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public async Task<IActionResult> SetupEventLevels(int id)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var tournament = await _tournamentService.GetByIdAsync(id);
|
|
||||||
if (tournament == null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
var eventLevels = await _tournamentService.GetEventLevelsAsync(id);
|
|
||||||
var events = await _eventService.GetAllAsync();
|
|
||||||
|
|
||||||
ViewBag.Tournament = tournament;
|
|
||||||
ViewBag.Events = events;
|
|
||||||
ViewBag.EventLevels = eventLevels;
|
|
||||||
|
|
||||||
return View(eventLevels);
|
|
||||||
}
|
|
||||||
catch (KeyNotFoundException)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> AddEventLevel(TournamentEventLevelCreateDto dto)
|
public async Task<IActionResult> AddEventLevel(TournamentEventLevelCreateDto dto)
|
||||||
@@ -202,12 +179,12 @@ public class TournamentController : Controller
|
|||||||
TempData["ErrorMessage"] = ex.Message;
|
TempData["ErrorMessage"] = ex.Message;
|
||||||
}
|
}
|
||||||
|
|
||||||
return RedirectToAction(nameof(SetupEventLevels), new { id = dto.TournamentId });
|
return RedirectToAction(nameof(Details), new { id = dto.TournamentId });
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> RemoveEventLevel(int tournamentEventLevelId, int tournamentId)
|
public async Task<IActionResult> RemoveEventLevel(int tournamentEventLevelId, int id)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -223,7 +200,7 @@ public class TournamentController : Controller
|
|||||||
TempData["ErrorMessage"] = ex.Message;
|
TempData["ErrorMessage"] = ex.Message;
|
||||||
}
|
}
|
||||||
|
|
||||||
return RedirectToAction(nameof(SetupEventLevels), new { id = tournamentId });
|
return RedirectToAction(nameof(Details), new { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
@@ -291,7 +268,7 @@ public class TournamentController : Controller
|
|||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> ToggleAgeWaiver(int tournamentEventLevelId, int tournamentId)
|
public async Task<IActionResult> ToggleAgeWaiver(int tournamentEventLevelId, int id)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -307,6 +284,6 @@ public class TournamentController : Controller
|
|||||||
TempData["ErrorMessage"] = ex.Message;
|
TempData["ErrorMessage"] = ex.Message;
|
||||||
}
|
}
|
||||||
|
|
||||||
return RedirectToAction(nameof(SetupEventLevels), new { id = tournamentId });
|
return RedirectToAction(nameof(Details), new { id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,22 +12,49 @@ public class TrackEventController : Controller
|
|||||||
private readonly IHeatManagementService _heatManagementService;
|
private readonly IHeatManagementService _heatManagementService;
|
||||||
private readonly ITournamentService _tournamentService;
|
private readonly ITournamentService _tournamentService;
|
||||||
private readonly IScoringService _scoringService;
|
private readonly IScoringService _scoringService;
|
||||||
|
private readonly IRegistrationService _registrationService;
|
||||||
|
|
||||||
public TrackEventController(
|
public TrackEventController(
|
||||||
IHeatManagementService heatManagementService,
|
IHeatManagementService heatManagementService,
|
||||||
ITournamentService tournamentService,
|
ITournamentService tournamentService,
|
||||||
IScoringService scoringService)
|
IScoringService scoringService,
|
||||||
|
IRegistrationService registrationService)
|
||||||
{
|
{
|
||||||
_heatManagementService = heatManagementService;
|
_heatManagementService = heatManagementService;
|
||||||
_tournamentService = tournamentService;
|
_tournamentService = tournamentService;
|
||||||
_scoringService = scoringService;
|
_scoringService = scoringService;
|
||||||
|
_registrationService = registrationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> Index(int tournamentEventLevelId)
|
public async Task<IActionResult> Index(int tournamentEventLevelId)
|
||||||
{
|
{
|
||||||
|
if (tournamentEventLevelId <= 0)
|
||||||
|
{
|
||||||
|
ViewBag.ScoringTargets = await _tournamentService.GetEventLevelsByCategoryAsync(EventCategory.Track);
|
||||||
|
ViewBag.ScoringController = "TrackEvent";
|
||||||
|
ViewBag.ScoringTitle = "Track Event Scoring";
|
||||||
|
return View("SelectEventLevel");
|
||||||
|
}
|
||||||
var rounds = await _heatManagementService.GetRoundsAsync(tournamentEventLevelId);
|
var rounds = await _heatManagementService.GetRoundsAsync(tournamentEventLevelId);
|
||||||
ViewBag.TournamentEventLevelId = tournamentEventLevelId;
|
ViewBag.TournamentEventLevelId = tournamentEventLevelId;
|
||||||
|
|
||||||
|
var tel = await _tournamentService.GetEventLevelByIdAsync(tournamentEventLevelId);
|
||||||
|
if (tel != null)
|
||||||
|
{
|
||||||
|
ViewBag.EventName = tel.EventName;
|
||||||
|
ViewBag.LevelName = tel.EventLevelName;
|
||||||
|
ViewBag.TournamentName = tel.TournamentName;
|
||||||
|
ViewBag.RegistrationCount = tel.RegistrationCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final standings (placement points are awarded from the final round).
|
||||||
|
var registrations = await _registrationService.GetByTournamentEventLevelAsync(tournamentEventLevelId);
|
||||||
|
ViewBag.Standings = registrations
|
||||||
|
.Where(r => r.Score?.Placement != null)
|
||||||
|
.OrderBy(r => r.Score!.Placement)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
return View(rounds);
|
return View(rounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +67,10 @@ public class TrackEventController : Controller
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ViewBag.TournamentEventLevelId = round.TournamentEventLevelId;
|
||||||
|
var rounds = await _heatManagementService.GetRoundsAsync(round.TournamentEventLevelId);
|
||||||
|
ViewBag.HasNextRound = rounds.Any(r => r.RoundOrder == round.RoundOrder + 1);
|
||||||
|
|
||||||
return View(round);
|
return View(round);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,11 +96,13 @@ public class TrackEventController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> SaveTimes(int heatId, [FromBody] List<HeatLaneUpdateDto> lanes)
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> SaveTimes(int heatId, int roundId, List<HeatLaneUpdateDto> lanes)
|
||||||
{
|
{
|
||||||
var recordedBy = User.Identity?.Name ?? "Unknown";
|
var recordedBy = User.Identity?.Name ?? "Unknown";
|
||||||
await _heatManagementService.SaveHeatTimesAsync(heatId, lanes, recordedBy);
|
await _heatManagementService.SaveHeatTimesAsync(heatId, lanes, recordedBy);
|
||||||
return Json(new { success = true });
|
TempData["SuccessMessage"] = "Heat times saved.";
|
||||||
|
return RedirectToAction(nameof(ManageRound), new { roundId });
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
@@ -83,9 +116,18 @@ public class TrackEventController : Controller
|
|||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> PopulateNextRound(int roundId)
|
public async Task<IActionResult> PopulateNextRound(int roundId)
|
||||||
|
{
|
||||||
|
var round = await _heatManagementService.GetRoundWithHeatsAsync(roundId);
|
||||||
|
try
|
||||||
{
|
{
|
||||||
await _heatManagementService.PopulateNextRoundAsync(roundId);
|
await _heatManagementService.PopulateNextRoundAsync(roundId);
|
||||||
return RedirectToAction(nameof(ManageRound), new { roundId });
|
TempData["SuccessMessage"] = "Advancing athletes moved into the next round.";
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
TempData["ErrorMessage"] = ex.Message;
|
||||||
|
}
|
||||||
|
return RedirectToAction(nameof(Index), new { tournamentEventLevelId = round?.TournamentEventLevelId });
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
@@ -103,4 +145,26 @@ public class TrackEventController : Controller
|
|||||||
await _heatManagementService.CompleteRoundAsync(roundId);
|
await _heatManagementService.CompleteRoundAsync(roundId);
|
||||||
return RedirectToAction(nameof(ManageRound), new { roundId });
|
return RedirectToAction(nameof(ManageRound), new { roundId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> CalculateFinalStandings(int tournamentEventLevelId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var recordedBy = User.Identity?.Name ?? "Unknown";
|
||||||
|
await _scoringService.CalculateTrackFinalResultsAsync(tournamentEventLevelId, recordedBy);
|
||||||
|
TempData["SuccessMessage"] = "Final standings calculated and placement points awarded.";
|
||||||
|
}
|
||||||
|
catch (KeyNotFoundException)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
TempData["ErrorMessage"] = ex.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(Index), new { tournamentEventLevelId });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
37
src/SportsDivision.Web/Helpers/PaginationHelper.cs
Normal file
37
src/SportsDivision.Web/Helpers/PaginationHelper.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace SportsDivision.Web.Helpers;
|
||||||
|
|
||||||
|
public static class PaginationHelper
|
||||||
|
{
|
||||||
|
public const int PageSize = 20;
|
||||||
|
|
||||||
|
/// <summary>Page-size choices offered in the UI. 0 means "All".</summary>
|
||||||
|
public static readonly int[] PageSizeOptions = { 20, 50, 100, 500, 0 };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Slices <paramref name="source"/> to the requested page and records paging
|
||||||
|
/// metadata in ViewData (read as ViewBag.Page / ViewBag.TotalPages / ViewBag.PageSize
|
||||||
|
/// in views). A <paramref name="pageSize"/> of 0 (or less) means "show all".
|
||||||
|
/// </summary>
|
||||||
|
public static List<T> Page<T>(this Controller controller, IEnumerable<T> source, int page, int pageSize = PageSize)
|
||||||
|
{
|
||||||
|
var list = source as IList<T> ?? source.ToList();
|
||||||
|
|
||||||
|
// Only the offered choices are honoured; anything else falls back to the default.
|
||||||
|
if (!PageSizeOptions.Contains(pageSize)) pageSize = PageSize;
|
||||||
|
var showAll = pageSize <= 0;
|
||||||
|
|
||||||
|
var totalPages = showAll ? 1 : (int)Math.Ceiling(list.Count / (double)pageSize);
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (totalPages > 0 && page > totalPages) page = totalPages;
|
||||||
|
|
||||||
|
controller.ViewData["Page"] = page;
|
||||||
|
controller.ViewData["TotalPages"] = totalPages;
|
||||||
|
controller.ViewData["TotalItems"] = list.Count;
|
||||||
|
controller.ViewData["PageSize"] = pageSize; // 0 == All
|
||||||
|
|
||||||
|
if (showAll) return list.ToList();
|
||||||
|
return list.Skip((page - 1) * pageSize).Take(pageSize).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
192
src/SportsDivision.Web/Reports/ReportExcelExporter.cs
Normal file
192
src/SportsDivision.Web/Reports/ReportExcelExporter.cs
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
using ClosedXML.Excel;
|
||||||
|
using SportsDivision.Application.DTOs;
|
||||||
|
using SportsDivision.Application.DTOs.ReportDtos;
|
||||||
|
|
||||||
|
namespace SportsDivision.Web.Reports;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds Microsoft Excel (.xlsx) workbooks for each report. One worksheet per
|
||||||
|
/// report, with a title, subtitle (tournament name), a styled header row, and
|
||||||
|
/// flattened data rows for the nested reports.
|
||||||
|
/// </summary>
|
||||||
|
public static class ReportExcelExporter
|
||||||
|
{
|
||||||
|
public const string MimeType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
||||||
|
|
||||||
|
private static int WriteHeader(IXLWorksheet ws, string title, string subtitle, string[] columns)
|
||||||
|
{
|
||||||
|
ws.Cell(1, 1).Value = title;
|
||||||
|
ws.Cell(1, 1).Style.Font.Bold = true;
|
||||||
|
ws.Cell(1, 1).Style.Font.FontSize = 14;
|
||||||
|
ws.Range(1, 1, 1, Math.Max(1, columns.Length)).Merge();
|
||||||
|
|
||||||
|
ws.Cell(2, 1).Value = subtitle;
|
||||||
|
ws.Cell(2, 1).Style.Font.Italic = true;
|
||||||
|
ws.Range(2, 1, 2, Math.Max(1, columns.Length)).Merge();
|
||||||
|
|
||||||
|
const int headerRow = 4;
|
||||||
|
for (int i = 0; i < columns.Length; i++)
|
||||||
|
{
|
||||||
|
var c = ws.Cell(headerRow, i + 1);
|
||||||
|
c.Value = columns[i];
|
||||||
|
c.Style.Font.Bold = true;
|
||||||
|
c.Style.Fill.BackgroundColor = XLColor.FromHtml("#003366");
|
||||||
|
c.Style.Font.FontColor = XLColor.White;
|
||||||
|
}
|
||||||
|
return headerRow + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] Finish(XLWorkbook wb, params IXLWorksheet[] sheets)
|
||||||
|
{
|
||||||
|
foreach (var ws in sheets)
|
||||||
|
{
|
||||||
|
ws.SheetView.FreezeRows(4);
|
||||||
|
ws.Columns().AdjustToContents();
|
||||||
|
}
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
wb.SaveAs(ms);
|
||||||
|
return ms.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetInt(IXLCell cell, int? v) { if (v.HasValue) cell.Value = v.Value; else cell.Value = "-"; }
|
||||||
|
private static void SetDec(IXLCell cell, decimal? v) { if (v.HasValue) cell.Value = v.Value; else cell.Value = "-"; }
|
||||||
|
|
||||||
|
public static byte[] PopularEvents(IEnumerable<PopularEventsReportDto> data, string tournamentName)
|
||||||
|
{
|
||||||
|
using var wb = new XLWorkbook();
|
||||||
|
var ws = wb.AddWorksheet("Popular Events");
|
||||||
|
int r = WriteHeader(ws, "Popular Events Report", tournamentName,
|
||||||
|
new[] { "Event", "Category", "Total", "Male", "Female" });
|
||||||
|
foreach (var e in data)
|
||||||
|
{
|
||||||
|
ws.Cell(r, 1).Value = e.EventName;
|
||||||
|
ws.Cell(r, 2).Value = e.Category;
|
||||||
|
ws.Cell(r, 3).Value = e.RegistrationCount;
|
||||||
|
ws.Cell(r, 4).Value = e.MaleCount;
|
||||||
|
ws.Cell(r, 5).Value = e.FemaleCount;
|
||||||
|
r++;
|
||||||
|
}
|
||||||
|
return Finish(wb, ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] RegistrationByGender(IEnumerable<RegistrationByGenderReportDto> data, string tournamentName)
|
||||||
|
{
|
||||||
|
using var wb = new XLWorkbook();
|
||||||
|
var ws = wb.AddWorksheet("Registration by Gender");
|
||||||
|
int r = WriteHeader(ws, "Registration by Gender", tournamentName,
|
||||||
|
new[] { "Event", "Level", "Male", "Female", "Total" });
|
||||||
|
foreach (var e in data)
|
||||||
|
{
|
||||||
|
ws.Cell(r, 1).Value = e.EventName;
|
||||||
|
ws.Cell(r, 2).Value = e.EventLevelName;
|
||||||
|
ws.Cell(r, 3).Value = e.MaleCount;
|
||||||
|
ws.Cell(r, 4).Value = e.FemaleCount;
|
||||||
|
ws.Cell(r, 5).Value = e.TotalCount;
|
||||||
|
r++;
|
||||||
|
}
|
||||||
|
return Finish(wb, ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] EventSchoolReport(IEnumerable<EventSchoolReportDto> data, string tournamentName)
|
||||||
|
{
|
||||||
|
using var wb = new XLWorkbook();
|
||||||
|
var ws = wb.AddWorksheet("Event School Report");
|
||||||
|
int r = WriteHeader(ws, "Event School Report", tournamentName,
|
||||||
|
new[] { "Event", "Level", "School", "Student", "Performance", "Placement", "Points" });
|
||||||
|
foreach (var ev in data)
|
||||||
|
foreach (var school in ev.Schools)
|
||||||
|
foreach (var s in school.Students)
|
||||||
|
{
|
||||||
|
ws.Cell(r, 1).Value = ev.EventName;
|
||||||
|
ws.Cell(r, 2).Value = ev.EventLevelName;
|
||||||
|
ws.Cell(r, 3).Value = school.SchoolName;
|
||||||
|
ws.Cell(r, 4).Value = s.StudentName;
|
||||||
|
SetDec(ws.Cell(r, 5), s.RawPerformance);
|
||||||
|
SetInt(ws.Cell(r, 6), s.Placement);
|
||||||
|
ws.Cell(r, 7).Value = s.PlacementPoints;
|
||||||
|
r++;
|
||||||
|
}
|
||||||
|
return Finish(wb, ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] StudentsBySchool(IEnumerable<StudentsBySchoolReportDto> data, string tournamentName)
|
||||||
|
{
|
||||||
|
using var wb = new XLWorkbook();
|
||||||
|
var ws = wb.AddWorksheet("Students by School");
|
||||||
|
int r = WriteHeader(ws, "Students by School", tournamentName,
|
||||||
|
new[] { "School", "Zone", "Student", "Sex", "Events" });
|
||||||
|
foreach (var school in data)
|
||||||
|
foreach (var s in school.Students)
|
||||||
|
{
|
||||||
|
ws.Cell(r, 1).Value = school.SchoolName;
|
||||||
|
ws.Cell(r, 2).Value = school.ZoneName;
|
||||||
|
ws.Cell(r, 3).Value = s.StudentName;
|
||||||
|
ws.Cell(r, 4).Value = s.Sex;
|
||||||
|
ws.Cell(r, 5).Value = string.Join(", ", s.Events);
|
||||||
|
r++;
|
||||||
|
}
|
||||||
|
return Finish(wb, ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] ScoresByEvent(IEnumerable<ScoresByEventReportDto> data, string tournamentName)
|
||||||
|
{
|
||||||
|
using var wb = new XLWorkbook();
|
||||||
|
var ws = wb.AddWorksheet("Scores by Event");
|
||||||
|
int r = WriteHeader(ws, "Scores by Event", tournamentName,
|
||||||
|
new[] { "Event", "Level", "Category", "Placement", "Student", "School", "Performance", "WA Points", "Placement Points" });
|
||||||
|
foreach (var ev in data)
|
||||||
|
foreach (var s in ev.Scores)
|
||||||
|
{
|
||||||
|
ws.Cell(r, 1).Value = ev.EventName;
|
||||||
|
ws.Cell(r, 2).Value = ev.EventLevelName;
|
||||||
|
ws.Cell(r, 3).Value = ev.Category;
|
||||||
|
SetInt(ws.Cell(r, 4), s.Placement);
|
||||||
|
ws.Cell(r, 5).Value = s.StudentName;
|
||||||
|
ws.Cell(r, 6).Value = s.SchoolName;
|
||||||
|
ws.Cell(r, 7).Value = s.RawPerformance;
|
||||||
|
ws.Cell(r, 8).Value = s.CalculatedPoints;
|
||||||
|
ws.Cell(r, 9).Value = s.PlacementPoints;
|
||||||
|
r++;
|
||||||
|
}
|
||||||
|
return Finish(wb, ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] StudentPoints(IEnumerable<StudentPointsReportDto> data, string tournamentName)
|
||||||
|
{
|
||||||
|
using var wb = new XLWorkbook();
|
||||||
|
var ws = wb.AddWorksheet("Student Points");
|
||||||
|
int r = WriteHeader(ws, "Student Points", tournamentName,
|
||||||
|
new[] { "Student", "School", "Sex", "Total Placement Points", "Events" });
|
||||||
|
foreach (var s in data)
|
||||||
|
{
|
||||||
|
ws.Cell(r, 1).Value = s.StudentName;
|
||||||
|
ws.Cell(r, 2).Value = s.SchoolName;
|
||||||
|
ws.Cell(r, 3).Value = s.Sex;
|
||||||
|
ws.Cell(r, 4).Value = s.TotalPlacementPoints;
|
||||||
|
ws.Cell(r, 5).Value = s.EventCount;
|
||||||
|
r++;
|
||||||
|
}
|
||||||
|
return Finish(wb, ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] SchoolStandings(IEnumerable<SchoolPointsSummaryDto> data, string tournamentName)
|
||||||
|
{
|
||||||
|
using var wb = new XLWorkbook();
|
||||||
|
var ws = wb.AddWorksheet("School Standings");
|
||||||
|
int r = WriteHeader(ws, "School Standings", tournamentName,
|
||||||
|
new[] { "Rank", "School", "Short Name", "Total Points", "1st Place", "2nd Place", "3rd Place" });
|
||||||
|
int rank = 1;
|
||||||
|
foreach (var s in data)
|
||||||
|
{
|
||||||
|
ws.Cell(r, 1).Value = rank++;
|
||||||
|
ws.Cell(r, 2).Value = s.SchoolName;
|
||||||
|
ws.Cell(r, 3).Value = s.ShortName ?? string.Empty;
|
||||||
|
ws.Cell(r, 4).Value = s.TotalPoints;
|
||||||
|
ws.Cell(r, 5).Value = s.FirstPlaceCount;
|
||||||
|
ws.Cell(r, 6).Value = s.SecondPlaceCount;
|
||||||
|
ws.Cell(r, 7).Value = s.ThirdPlaceCount;
|
||||||
|
r++;
|
||||||
|
}
|
||||||
|
return Finish(wb, ws);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="ClosedXML" Version="0.105.0" />
|
||||||
<PackageReference Include="QuestPDF" Version="2024.*" />
|
<PackageReference Include="QuestPDF" Version="2024.*" />
|
||||||
<PackageReference Include="FluentValidation.AspNetCore" Version="11.*" />
|
<PackageReference Include="FluentValidation.AspNetCore" Version="11.*" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.*">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.*">
|
||||||
|
|||||||
41
src/SportsDivision.Web/Views/Event/Create.cshtml
Normal file
41
src/SportsDivision.Web/Views/Event/Create.cshtml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
@using SportsDivision.Domain.Enums
|
||||||
|
@model EventCreateDto
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Add Event";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2>Add Event</h2>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<form asp-action="Create" method="post">
|
||||||
|
<div asp-validation-summary="All" class="text-danger"></div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label asp-for="Name" class="form-label">Name</label>
|
||||||
|
<input asp-for="Name" class="form-control" />
|
||||||
|
<span asp-validation-for="Name" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label asp-for="Category" class="form-label">Category</label>
|
||||||
|
<select asp-for="Category" asp-items="Html.GetEnumSelectList<EventCategory>()" class="form-select"></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input asp-for="IsRelay" class="form-check-input" />
|
||||||
|
<label asp-for="IsRelay" class="form-check-label">Relay event</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input asp-for="PrimarySchool" class="form-check-input" />
|
||||||
|
<label asp-for="PrimarySchool" class="form-check-label">Available to primary schools</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input asp-for="SecondarySchool" class="form-check-input" />
|
||||||
|
<label asp-for="SecondarySchool" class="form-check-label">Available to secondary schools</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Create</button>
|
||||||
|
<a asp-action="Index" class="btn btn-secondary">Cancel</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts { <partial name="_ValidationScriptsPartial" /> }
|
||||||
50
src/SportsDivision.Web/Views/Event/Edit.cshtml
Normal file
50
src/SportsDivision.Web/Views/Event/Edit.cshtml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
@using SportsDivision.Domain.Enums
|
||||||
|
@model EventUpdateDto
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Edit Event";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2>Edit Event</h2>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<form asp-action="Edit" method="post">
|
||||||
|
<div asp-validation-summary="All" class="text-danger"></div>
|
||||||
|
<input type="hidden" asp-for="EventId" />
|
||||||
|
@for (var i = 0; i < Model.EventLevelIds.Count; i++)
|
||||||
|
{
|
||||||
|
<input type="hidden" asp-for="EventLevelIds[i]" />
|
||||||
|
}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label asp-for="Name" class="form-label">Name</label>
|
||||||
|
<input asp-for="Name" class="form-control" />
|
||||||
|
<span asp-validation-for="Name" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label asp-for="Category" class="form-label">Category</label>
|
||||||
|
<select asp-for="Category" asp-items="Html.GetEnumSelectList<EventCategory>()" class="form-select"></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input asp-for="IsRelay" class="form-check-input" />
|
||||||
|
<label asp-for="IsRelay" class="form-check-label">Relay event</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input asp-for="PrimarySchool" class="form-check-input" />
|
||||||
|
<label asp-for="PrimarySchool" class="form-check-label">Available to primary schools</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input asp-for="SecondarySchool" class="form-check-input" />
|
||||||
|
<label asp-for="SecondarySchool" class="form-check-label">Available to secondary schools</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input asp-for="IsActive" class="form-check-input" />
|
||||||
|
<label asp-for="IsActive" class="form-check-label">Active</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Save</button>
|
||||||
|
<a asp-action="Index" class="btn btn-secondary">Cancel</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts { <partial name="_ValidationScriptsPartial" /> }
|
||||||
@@ -35,3 +35,5 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<partial name="_Pagination" />
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
<td>@r.SchoolName</td>
|
<td>@r.SchoolName</td>
|
||||||
<td>
|
<td>
|
||||||
<form asp-action="RecordScore" method="post" class="d-flex gap-1">
|
<form asp-action="RecordScore" method="post" class="d-flex gap-1">
|
||||||
|
<input type="hidden" name="tournamentEventLevelId" value="@telId" />
|
||||||
<input type="hidden" name="EventRegistrationId" value="@r.EventRegistrationId" />
|
<input type="hidden" name="EventRegistrationId" value="@r.EventRegistrationId" />
|
||||||
<input type="number" step="0.01" name="RawPerformance" value="@r.Score?.RawPerformance" class="form-control form-control-sm" style="width:100px" />
|
<input type="number" step="0.01" name="RawPerformance" value="@r.Score?.RawPerformance" class="form-control form-control-sm" style="width:100px" />
|
||||||
<button type="submit" class="btn btn-sm btn-outline-primary">Save</button>
|
<button type="submit" class="btn btn-sm btn-outline-primary">Save</button>
|
||||||
|
|||||||
@@ -73,6 +73,7 @@
|
|||||||
@for (int i = 1; i <= 3; i++)
|
@for (int i = 1; i <= 3; i++)
|
||||||
{
|
{
|
||||||
<form asp-action="RecordAttempt" method="post" class="d-inline">
|
<form asp-action="RecordAttempt" method="post" class="d-inline">
|
||||||
|
<input type="hidden" name="tournamentEventLevelId" value="@telId" />
|
||||||
<input type="hidden" name="HighJumpHeightId" value="@height.HighJumpHeightId" />
|
<input type="hidden" name="HighJumpHeightId" value="@height.HighJumpHeightId" />
|
||||||
<input type="hidden" name="EventRegistrationId" value="@reg.EventRegistrationId" />
|
<input type="hidden" name="EventRegistrationId" value="@reg.EventRegistrationId" />
|
||||||
<input type="hidden" name="AttemptNumber" value="@i" />
|
<input type="hidden" name="AttemptNumber" value="@i" />
|
||||||
|
|||||||
@@ -3,7 +3,13 @@
|
|||||||
ViewData["Title"] = "Event School Report";
|
ViewData["Title"] = "Event School Report";
|
||||||
}
|
}
|
||||||
|
|
||||||
<h2>Event School Report</h2>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h2 class="mb-0">Event School Report</h2>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a asp-action="EventSchoolReportExcel" asp-route-tournamentId="@ViewBag.TournamentId" asp-route-eventId="@ViewBag.EventId" asp-route-eventLevelId="@ViewBag.EventLevelId" class="btn btn-success"><i class="bi bi-file-earmark-excel"></i> Export to Excel</a>
|
||||||
|
<a asp-action="EventSchoolReportPdf" asp-route-tournamentId="@ViewBag.TournamentId" asp-route-eventId="@ViewBag.EventId" asp-route-eventLevelId="@ViewBag.EventLevelId" class="btn btn-outline-danger"><i class="bi bi-file-earmark-pdf"></i> PDF</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
@foreach (var evt in Model)
|
@foreach (var evt in Model)
|
||||||
|
|||||||
@@ -3,7 +3,13 @@
|
|||||||
ViewData["Title"] = "Popular Events";
|
ViewData["Title"] = "Popular Events";
|
||||||
}
|
}
|
||||||
|
|
||||||
<h2>Popular Events Report</h2>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h2 class="mb-0">Popular Events Report</h2>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a asp-action="PopularEventsExcel" asp-route-tournamentId="@ViewBag.TournamentId" class="btn btn-success"><i class="bi bi-file-earmark-excel"></i> Export to Excel</a>
|
||||||
|
<a asp-action="PopularEventsPdf" asp-route-tournamentId="@ViewBag.TournamentId" class="btn btn-outline-danger"><i class="bi bi-file-earmark-pdf"></i> PDF</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<table class="table table-striped table-hover">
|
<table class="table table-striped table-hover">
|
||||||
|
|||||||
@@ -3,7 +3,13 @@
|
|||||||
ViewData["Title"] = "Registration by Gender";
|
ViewData["Title"] = "Registration by Gender";
|
||||||
}
|
}
|
||||||
|
|
||||||
<h2>Registration by Gender</h2>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h2 class="mb-0">Registration by Gender</h2>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a asp-action="RegistrationByGenderExcel" asp-route-tournamentId="@ViewBag.TournamentId" asp-route-zoneId="@ViewBag.ZoneId" class="btn btn-success"><i class="bi bi-file-earmark-excel"></i> Export to Excel</a>
|
||||||
|
<a asp-action="RegistrationByGenderPdf" asp-route-tournamentId="@ViewBag.TournamentId" asp-route-zoneId="@ViewBag.ZoneId" class="btn btn-outline-danger"><i class="bi bi-file-earmark-pdf"></i> PDF</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<table class="table table-striped table-hover">
|
<table class="table table-striped table-hover">
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
ViewData["Title"] = "School Standings";
|
ViewData["Title"] = "School Standings";
|
||||||
}
|
}
|
||||||
|
|
||||||
<h2>School Standings</h2>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h2 class="mb-0">School Standings</h2>
|
||||||
|
<a asp-action="SchoolStandingsExcel" asp-route-tournamentId="@ViewBag.TournamentId" class="btn btn-success"><i class="bi bi-file-earmark-excel"></i> Export to Excel</a>
|
||||||
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
|
|||||||
@@ -3,7 +3,13 @@
|
|||||||
ViewData["Title"] = "Scores by Event";
|
ViewData["Title"] = "Scores by Event";
|
||||||
}
|
}
|
||||||
|
|
||||||
<h2>Scores by Event</h2>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h2 class="mb-0">Scores by Event</h2>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a asp-action="ScoresByEventExcel" asp-route-tournamentId="@ViewBag.TournamentId" asp-route-eventId="@ViewBag.EventId" asp-route-eventLevelId="@ViewBag.EventLevelId" class="btn btn-success"><i class="bi bi-file-earmark-excel"></i> Export to Excel</a>
|
||||||
|
<a asp-action="ScoresByEventPdf" asp-route-tournamentId="@ViewBag.TournamentId" asp-route-eventId="@ViewBag.EventId" asp-route-eventLevelId="@ViewBag.EventLevelId" class="btn btn-outline-danger"><i class="bi bi-file-earmark-pdf"></i> PDF</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
@foreach (var evt in Model)
|
@foreach (var evt in Model)
|
||||||
|
|||||||
@@ -3,7 +3,13 @@
|
|||||||
ViewData["Title"] = "Student Points";
|
ViewData["Title"] = "Student Points";
|
||||||
}
|
}
|
||||||
|
|
||||||
<h2>Student Points</h2>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h2 class="mb-0">Student Points</h2>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a asp-action="StudentPointsExcel" asp-route-tournamentId="@ViewBag.TournamentId" asp-route-schoolId="@ViewBag.SchoolId" asp-route-zoneId="@ViewBag.ZoneId" class="btn btn-success"><i class="bi bi-file-earmark-excel"></i> Export to Excel</a>
|
||||||
|
<a asp-action="StudentPointsPdf" asp-route-tournamentId="@ViewBag.TournamentId" asp-route-schoolId="@ViewBag.SchoolId" asp-route-zoneId="@ViewBag.ZoneId" class="btn btn-outline-danger"><i class="bi bi-file-earmark-pdf"></i> PDF</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<table class="table table-striped table-hover">
|
<table class="table table-striped table-hover">
|
||||||
|
|||||||
@@ -3,7 +3,13 @@
|
|||||||
ViewData["Title"] = "Students by School";
|
ViewData["Title"] = "Students by School";
|
||||||
}
|
}
|
||||||
|
|
||||||
<h2>Students by School</h2>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h2 class="mb-0">Students by School</h2>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a asp-action="StudentsBySchoolExcel" asp-route-tournamentId="@ViewBag.TournamentId" asp-route-schoolId="@ViewBag.SchoolId" asp-route-zoneId="@ViewBag.ZoneId" class="btn btn-success"><i class="bi bi-file-earmark-excel"></i> Export to Excel</a>
|
||||||
|
<a asp-action="StudentsBySchoolPdf" asp-route-tournamentId="@ViewBag.TournamentId" asp-route-schoolId="@ViewBag.SchoolId" asp-route-zoneId="@ViewBag.ZoneId" class="btn btn-outline-danger"><i class="bi bi-file-earmark-pdf"></i> PDF</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
@foreach (var school in Model)
|
@foreach (var school in Model)
|
||||||
|
|||||||
45
src/SportsDivision.Web/Views/School/Details.cshtml
Normal file
45
src/SportsDivision.Web/Views/School/Details.cshtml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
@model SchoolDto
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = Model.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h2>@Model.Name</h2>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a asp-action="Edit" asp-route-id="@Model.SchoolId" class="btn btn-outline-primary">Edit</a>
|
||||||
|
<a asp-controller="Student" asp-action="Index" asp-route-schoolId="@Model.SchoolId" class="btn btn-outline-secondary">Students</a>
|
||||||
|
<a asp-action="Index" class="btn btn-secondary">Back</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<partial name="_Notification" />
|
||||||
|
|
||||||
|
<div class="card shadow-sm" style="max-width: 600px;">
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row mb-0">
|
||||||
|
<dt class="col-sm-4">Short Name</dt>
|
||||||
|
<dd class="col-sm-8">@(string.IsNullOrEmpty(Model.ShortName) ? "—" : Model.ShortName)</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-4">Level</dt>
|
||||||
|
<dd class="col-sm-8">@Model.SchoolLevel</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-4">Zone</dt>
|
||||||
|
<dd class="col-sm-8">@Model.ZoneName</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-4">Students</dt>
|
||||||
|
<dd class="col-sm-8">@Model.StudentCount</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-4">Status</dt>
|
||||||
|
<dd class="col-sm-8">
|
||||||
|
@if (Model.IsActive)
|
||||||
|
{
|
||||||
|
<span class="badge bg-success">Active</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="badge bg-secondary">Inactive</span>
|
||||||
|
}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -61,3 +61,5 @@
|
|||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<partial name="_Pagination" />
|
||||||
|
|||||||
@@ -16,6 +16,15 @@
|
|||||||
<h5 class="mb-0">Scoring Constants (World Athletics)</h5>
|
<h5 class="mb-0">Scoring Constants (World Athletics)</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
@if (constants != null)
|
||||||
|
{
|
||||||
|
@foreach (var c in constants)
|
||||||
|
{
|
||||||
|
<form id="scf-@c.ScoringConstantId" asp-action="UpdateScoringConstant" method="post" class="d-none">
|
||||||
|
<input type="hidden" name="ScoringConstantId" value="@c.ScoringConstantId" />
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
}
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
<thead><tr><th>Event</th><th>A</th><th>B</th><th>C</th><th>Unit</th><th>Action</th></tr></thead>
|
<thead><tr><th>Event</th><th>A</th><th>B</th><th>C</th><th>Unit</th><th>Action</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -24,15 +33,12 @@
|
|||||||
@foreach (var c in constants)
|
@foreach (var c in constants)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<form asp-action="UpdateScoringConstant" method="post">
|
|
||||||
<input type="hidden" name="ScoringConstantId" value="@c.ScoringConstantId" />
|
|
||||||
<td>@c.EventName</td>
|
<td>@c.EventName</td>
|
||||||
<td><input type="number" step="0.00001" name="A" value="@c.A" class="form-control form-control-sm" style="width:100px" /></td>
|
<td><input type="number" step="0.00001" name="A" value="@c.A" form="scf-@c.ScoringConstantId" class="form-control form-control-sm" style="width:100px" /></td>
|
||||||
<td><input type="number" step="0.1" name="B" value="@c.B" class="form-control form-control-sm" style="width:80px" /></td>
|
<td><input type="number" step="0.1" name="B" value="@c.B" form="scf-@c.ScoringConstantId" class="form-control form-control-sm" style="width:80px" /></td>
|
||||||
<td><input type="number" step="0.01" name="C" value="@c.C" class="form-control form-control-sm" style="width:80px" /></td>
|
<td><input type="number" step="0.01" name="C" value="@c.C" form="scf-@c.ScoringConstantId" class="form-control form-control-sm" style="width:80px" /></td>
|
||||||
<td>@c.Unit</td>
|
<td>@c.Unit</td>
|
||||||
<td><button type="submit" class="btn btn-sm btn-outline-primary">Save</button></td>
|
<td><button type="submit" form="scf-@c.ScoringConstantId" class="btn btn-sm btn-outline-primary">Save</button></td>
|
||||||
</form>
|
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,6 +53,16 @@
|
|||||||
<h5 class="mb-0">Placement Points</h5>
|
<h5 class="mb-0">Placement Points</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
|
@if (placements != null)
|
||||||
|
{
|
||||||
|
@foreach (var p in placements.OrderBy(x => x.Placement))
|
||||||
|
{
|
||||||
|
<form id="ppf-@p.PlacementPointConfigId" asp-action="UpdatePlacementPointConfig" method="post" class="d-none">
|
||||||
|
<input type="hidden" name="PlacementPointConfigId" value="@p.PlacementPointConfigId" />
|
||||||
|
<input type="hidden" name="Placement" value="@p.Placement" />
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
}
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
<thead><tr><th>Place</th><th>Points</th><th>Action</th></tr></thead>
|
<thead><tr><th>Place</th><th>Points</th><th>Action</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -55,13 +71,9 @@
|
|||||||
@foreach (var p in placements.OrderBy(x => x.Placement))
|
@foreach (var p in placements.OrderBy(x => x.Placement))
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<form asp-action="UpdatePlacementPointConfig" method="post">
|
|
||||||
<input type="hidden" name="PlacementPointConfigId" value="@p.PlacementPointConfigId" />
|
|
||||||
<input type="hidden" name="Placement" value="@p.Placement" />
|
|
||||||
<td>#@p.Placement</td>
|
<td>#@p.Placement</td>
|
||||||
<td><input type="number" name="Points" value="@p.Points" class="form-control form-control-sm" style="width:60px" /></td>
|
<td><input type="number" name="Points" value="@p.Points" form="ppf-@p.PlacementPointConfigId" class="form-control form-control-sm" style="width:60px" /></td>
|
||||||
<td><button type="submit" class="btn btn-sm btn-outline-primary">Save</button></td>
|
<td><button type="submit" form="ppf-@p.PlacementPointConfigId" class="btn btn-sm btn-outline-primary">Save</button></td>
|
||||||
</form>
|
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
49
src/SportsDivision.Web/Views/Shared/SelectEventLevel.cshtml
Normal file
49
src/SportsDivision.Web/Views/Shared/SelectEventLevel.cshtml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
@{
|
||||||
|
var title = ViewBag.ScoringTitle as string ?? "Scoring";
|
||||||
|
var controller = ViewBag.ScoringController as string ?? "TrackEvent";
|
||||||
|
var targets = ViewBag.ScoringTargets as IEnumerable<TournamentEventLevelDto> ?? Enumerable.Empty<TournamentEventLevelDto>();
|
||||||
|
ViewData["Title"] = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2>@title</h2>
|
||||||
|
<p class="text-muted">Select an event level to score.</p>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
@if (!targets.Any())
|
||||||
|
{
|
||||||
|
<div class="alert alert-info">
|
||||||
|
No event levels are available for scoring yet. Add event levels to an active tournament from the
|
||||||
|
<a asp-controller="Tournament" asp-action="Index">Tournaments</a> page.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@foreach (var grp in targets.GroupBy(t => new { t.TournamentId, t.TournamentName }))
|
||||||
|
{
|
||||||
|
<div class="card shadow-sm mb-3">
|
||||||
|
<div class="card-header text-white" style="background-color: #003366;">
|
||||||
|
<h5 class="mb-0">@grp.Key.TournamentName</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Event</th><th>Level</th><th>Registrations</th><th></th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var t in grp.OrderBy(x => x.EventName).ThenBy(x => x.EventLevelName))
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@t.EventName</td>
|
||||||
|
<td>@t.EventLevelName</td>
|
||||||
|
<td>@t.RegistrationCount</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a asp-controller="@controller" asp-action="Index" asp-route-tournamentEventLevelId="@t.TournamentEventLevelId" class="btn btn-sm btn-outline-success">Score</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/SportsDivision.Web/Views/Shared/_Pagination.cshtml
Normal file
82
src/SportsDivision.Web/Views/Shared/_Pagination.cshtml
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
@{
|
||||||
|
var pg = ViewBag.Page as int? ?? 1;
|
||||||
|
var totalPages = ViewBag.TotalPages as int? ?? 0;
|
||||||
|
var totalItems = ViewBag.TotalItems as int? ?? 0;
|
||||||
|
var pageSize = ViewBag.PageSize as int? ?? 20; // 0 == All
|
||||||
|
var request = ViewContext.HttpContext.Request;
|
||||||
|
|
||||||
|
// Preserve existing query-string filters (zoneId, level, search, etc.), swapping out
|
||||||
|
// "page" and "pageSize".
|
||||||
|
var baseParams = request.Query
|
||||||
|
.Where(kv => kv.Key != "page" && kv.Key != "pageSize")
|
||||||
|
.SelectMany(kv => kv.Value.Select(v => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(v ?? string.Empty)}"))
|
||||||
|
.ToList();
|
||||||
|
Func<int, int, string> url = (p, ps) =>
|
||||||
|
{
|
||||||
|
var ps2 = new List<string>(baseParams) { $"pageSize={ps}", $"page={p}" };
|
||||||
|
return $"{request.Path}?{string.Join("&", ps2)}";
|
||||||
|
};
|
||||||
|
Func<int, string> pageUrl = p => url(p, pageSize);
|
||||||
|
|
||||||
|
var prevHref = pg <= 1 ? "#" : pageUrl(pg - 1);
|
||||||
|
var nextHref = pg >= totalPages ? "#" : pageUrl(pg + 1);
|
||||||
|
var start = Math.Max(1, pg - 2);
|
||||||
|
var end = Math.Min(totalPages, pg + 2);
|
||||||
|
|
||||||
|
var firstItem = totalItems == 0 ? 0 : (pageSize <= 0 ? 1 : (pg - 1) * pageSize + 1);
|
||||||
|
var lastItem = pageSize <= 0 ? totalItems : Math.Min(pg * pageSize, totalItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (totalItems > 0)
|
||||||
|
{
|
||||||
|
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mt-3">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span class="small text-muted">Show</span>
|
||||||
|
<select class="form-select form-select-sm" style="width:auto" onchange="location.href=this.value" aria-label="Items per page">
|
||||||
|
@foreach (var opt in new[] { 20, 50, 100, 500 })
|
||||||
|
{
|
||||||
|
<option value="@url(1, opt)" selected="@(pageSize == opt)">@opt</option>
|
||||||
|
}
|
||||||
|
<option value="@url(1, 0)" selected="@(pageSize <= 0)">All</option>
|
||||||
|
</select>
|
||||||
|
<span class="small text-muted">per page</span>
|
||||||
|
</div>
|
||||||
|
<span class="small text-muted">Showing @firstItem–@lastItem of @totalItems</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (totalPages > 1)
|
||||||
|
{
|
||||||
|
<nav aria-label="Page navigation" class="mt-2">
|
||||||
|
<ul class="pagination justify-content-center mb-0">
|
||||||
|
<li class="page-item @(pg <= 1 ? "disabled" : "")">
|
||||||
|
<a class="page-link" href="@prevHref">Previous</a>
|
||||||
|
</li>
|
||||||
|
@if (start > 1)
|
||||||
|
{
|
||||||
|
<li class="page-item"><a class="page-link" href="@pageUrl(1)">1</a></li>
|
||||||
|
@if (start > 2)
|
||||||
|
{
|
||||||
|
<li class="page-item disabled"><span class="page-link">…</span></li>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@for (int i = start; i <= end; i++)
|
||||||
|
{
|
||||||
|
<li class="page-item @(i == pg ? "active" : "")">
|
||||||
|
<a class="page-link" href="@pageUrl(i)">@i</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
@if (end < totalPages)
|
||||||
|
{
|
||||||
|
@if (end < totalPages - 1)
|
||||||
|
{
|
||||||
|
<li class="page-item disabled"><span class="page-link">…</span></li>
|
||||||
|
}
|
||||||
|
<li class="page-item"><a class="page-link" href="@pageUrl(totalPages)">@totalPages</a></li>
|
||||||
|
}
|
||||||
|
<li class="page-item @(pg >= totalPages ? "disabled" : "")">
|
||||||
|
<a class="page-link" href="@nextHref">Next</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,3 +56,5 @@
|
|||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<partial name="_Pagination" />
|
||||||
|
|||||||
@@ -6,12 +6,25 @@
|
|||||||
var levels = ViewBag.Levels as IEnumerable<EventLevelDto>;
|
var levels = ViewBag.Levels as IEnumerable<EventLevelDto>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@{
|
||||||
|
var statusClass = Model.Status switch {
|
||||||
|
TournamentStatus.Draft => "bg-secondary",
|
||||||
|
TournamentStatus.Registration => "bg-info",
|
||||||
|
TournamentStatus.InProgress => "bg-primary",
|
||||||
|
TournamentStatus.Completed => "bg-success",
|
||||||
|
_ => "bg-dark"
|
||||||
|
};
|
||||||
|
}
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<div>
|
<div>
|
||||||
<h2>@Model.Name</h2>
|
<h2 class="mb-1">
|
||||||
|
@Model.Name
|
||||||
|
<span class="badge @statusClass align-middle">@Model.Status</span>
|
||||||
|
@if (Model.IsArchived) { <span class="badge bg-dark align-middle">Archived</span> }
|
||||||
|
</h2>
|
||||||
<p class="text-muted mb-0">@Model.StartDate.ToString("MMMM d") - @Model.EndDate.ToString("MMMM d, yyyy")</p>
|
<p class="text-muted mb-0">@Model.StartDate.ToString("MMMM d") - @Model.EndDate.ToString("MMMM d, yyyy")</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2 flex-wrap justify-content-end">
|
||||||
@if (Model.Status == TournamentStatus.Draft)
|
@if (Model.Status == TournamentStatus.Draft)
|
||||||
{
|
{
|
||||||
<form asp-action="UpdateStatus" method="post">
|
<form asp-action="UpdateStatus" method="post">
|
||||||
@@ -27,16 +40,53 @@
|
|||||||
<input type="hidden" name="status" value="InProgress" />
|
<input type="hidden" name="status" value="InProgress" />
|
||||||
<button type="submit" class="btn btn-primary">Start Tournament</button>
|
<button type="submit" class="btn btn-primary">Start Tournament</button>
|
||||||
</form>
|
</form>
|
||||||
|
<form asp-action="UpdateStatus" method="post">
|
||||||
|
<input type="hidden" name="id" value="@Model.TournamentId" />
|
||||||
|
<input type="hidden" name="status" value="Draft" />
|
||||||
|
<button type="submit" class="btn btn-outline-secondary"
|
||||||
|
onclick="return confirm('Revert this tournament back to Draft?')">Back to Draft</button>
|
||||||
|
</form>
|
||||||
}
|
}
|
||||||
@if (Model.Status == TournamentStatus.InProgress)
|
@if (Model.Status == TournamentStatus.InProgress)
|
||||||
{
|
{
|
||||||
<form asp-action="UpdateStatus" method="post">
|
<form asp-action="UpdateStatus" method="post">
|
||||||
<input type="hidden" name="id" value="@Model.TournamentId" />
|
<input type="hidden" name="id" value="@Model.TournamentId" />
|
||||||
<input type="hidden" name="status" value="Completed" />
|
<input type="hidden" name="status" value="Completed" />
|
||||||
<button type="submit" class="btn btn-success">Complete</button>
|
<button type="submit" class="btn btn-success"
|
||||||
|
onclick="return confirm('Mark "@Model.Name" as Completed?\n\nYou can reopen it later if this was a mistake.')">Complete</button>
|
||||||
|
</form>
|
||||||
|
<form asp-action="UpdateStatus" method="post">
|
||||||
|
<input type="hidden" name="id" value="@Model.TournamentId" />
|
||||||
|
<input type="hidden" name="status" value="Registration" />
|
||||||
|
<button type="submit" class="btn btn-outline-secondary"
|
||||||
|
onclick="return confirm('Move this tournament back to Registration?')">Back to Registration</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
@if (Model.Status == TournamentStatus.Completed)
|
||||||
|
{
|
||||||
|
<form asp-action="UpdateStatus" method="post">
|
||||||
|
<input type="hidden" name="id" value="@Model.TournamentId" />
|
||||||
|
<input type="hidden" name="status" value="InProgress" />
|
||||||
|
<button type="submit" class="btn btn-warning"
|
||||||
|
onclick="return confirm('Reopen this completed tournament and set it back to In Progress?')">Reopen Tournament</button>
|
||||||
</form>
|
</form>
|
||||||
}
|
}
|
||||||
<a asp-action="Edit" asp-route-id="@Model.TournamentId" class="btn btn-outline-primary">Edit</a>
|
<a asp-action="Edit" asp-route-id="@Model.TournamentId" class="btn btn-outline-primary">Edit</a>
|
||||||
|
@if (!Model.IsArchived)
|
||||||
|
{
|
||||||
|
<form asp-action="Archive" method="post">
|
||||||
|
<input type="hidden" name="id" value="@Model.TournamentId" />
|
||||||
|
<button type="submit" class="btn btn-outline-dark"
|
||||||
|
onclick="return confirm('Archive this tournament? It will be hidden from the default list but not deleted, and can be restored later.')">Archive</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<form asp-action="Unarchive" method="post">
|
||||||
|
<input type="hidden" name="id" value="@Model.TournamentId" />
|
||||||
|
<button type="submit" class="btn btn-outline-dark">Unarchive</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -76,7 +126,15 @@
|
|||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a asp-controller="TrackEvent" asp-action="Index" asp-route-tournamentEventLevelId="@tel.TournamentEventLevelId" class="btn btn-sm btn-outline-success">Scoring</a>
|
@{
|
||||||
|
var scoringController = tel.Category switch
|
||||||
|
{
|
||||||
|
EventCategory.Field => "FieldEvent",
|
||||||
|
EventCategory.HighJump => "HighJump",
|
||||||
|
_ => "TrackEvent"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
<a asp-controller="@scoringController" asp-action="Index" asp-route-tournamentEventLevelId="@tel.TournamentEventLevelId" class="btn btn-sm btn-outline-success">Scoring</a>
|
||||||
<form asp-action="RemoveEventLevel" method="post" class="d-inline">
|
<form asp-action="RemoveEventLevel" method="post" class="d-inline">
|
||||||
<input type="hidden" name="tournamentEventLevelId" value="@tel.TournamentEventLevelId" />
|
<input type="hidden" name="tournamentEventLevelId" value="@tel.TournamentEventLevelId" />
|
||||||
<input type="hidden" name="id" value="@Model.TournamentId" />
|
<input type="hidden" name="id" value="@Model.TournamentId" />
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
@model IEnumerable<TournamentDto>
|
@model IEnumerable<TournamentDto>
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Tournaments";
|
ViewData["Title"] = "Tournaments";
|
||||||
|
var selectedStatus = ViewBag.SelectedStatus as TournamentStatus?;
|
||||||
|
var showArchived = ViewBag.ShowArchived as bool? ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
@@ -10,6 +12,26 @@
|
|||||||
|
|
||||||
<partial name="_Notification" />
|
<partial name="_Notification" />
|
||||||
|
|
||||||
|
<form asp-action="Index" method="get" class="row g-2 align-items-end mb-3">
|
||||||
|
<div class="col-auto">
|
||||||
|
<label class="form-label mb-0 small text-muted">Status</label>
|
||||||
|
<select name="status" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||||
|
<option value="">All statuses</option>
|
||||||
|
@foreach (var s in Enum.GetValues<TournamentStatus>())
|
||||||
|
{
|
||||||
|
<option value="@s" selected="@(selectedStatus == s)">@s</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="showArchived" value="true"
|
||||||
|
id="showArchived" @(showArchived ? "checked" : "") onchange="this.form.submit()" />
|
||||||
|
<label class="form-check-label" for="showArchived">Show archived</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<table class="table table-striped table-hover">
|
<table class="table table-striped table-hover">
|
||||||
<thead class="table-dark">
|
<thead class="table-dark">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -45,8 +67,25 @@
|
|||||||
<td>
|
<td>
|
||||||
<a asp-action="Edit" asp-route-id="@t.TournamentId" class="btn btn-sm btn-outline-primary">Edit</a>
|
<a asp-action="Edit" asp-route-id="@t.TournamentId" class="btn btn-sm btn-outline-primary">Edit</a>
|
||||||
<a asp-action="Details" asp-route-id="@t.TournamentId" class="btn btn-sm btn-outline-info">Details</a>
|
<a asp-action="Details" asp-route-id="@t.TournamentId" class="btn btn-sm btn-outline-info">Details</a>
|
||||||
|
@if (!t.IsArchived)
|
||||||
|
{
|
||||||
|
<form asp-action="Archive" method="post" class="d-inline">
|
||||||
|
<input type="hidden" name="id" value="@t.TournamentId" />
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-dark"
|
||||||
|
onclick="return confirm('Archive "@t.Name"? It will be hidden from the default list but can be restored.')">Archive</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<form asp-action="Unarchive" method="post" class="d-inline">
|
||||||
|
<input type="hidden" name="id" value="@t.TournamentId" />
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-dark">Unarchive</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<partial name="_Pagination" />
|
||||||
|
|||||||
@@ -4,13 +4,29 @@
|
|||||||
var telId = ViewBag.TournamentEventLevelId as int?;
|
var telId = ViewBag.TournamentEventLevelId as int?;
|
||||||
var eventName = ViewBag.EventName as string;
|
var eventName = ViewBag.EventName as string;
|
||||||
var levelName = ViewBag.LevelName as string;
|
var levelName = ViewBag.LevelName as string;
|
||||||
|
var tournamentName = ViewBag.TournamentName as string;
|
||||||
|
var regCount = ViewBag.RegistrationCount as int? ?? 0;
|
||||||
|
var standings = ViewBag.Standings as IEnumerable<EventRegistrationDto> ?? Enumerable.Empty<EventRegistrationDto>();
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<div>
|
<div>
|
||||||
<h2>Track Event Scoring</h2>
|
<h2 class="mb-1">@(string.IsNullOrEmpty(eventName) ? "Track Event Scoring" : $"{eventName} — {levelName}")</h2>
|
||||||
<p class="text-muted">@eventName - @levelName</p>
|
<p class="text-muted mb-0">
|
||||||
|
@if (!string.IsNullOrEmpty(tournamentName)) { <span><i class="bi bi-trophy"></i> @tournamentName</span> }
|
||||||
|
<span class="ms-2"><i class="bi bi-people"></i> @regCount registered</span>
|
||||||
|
<span class="ms-2"><i class="bi bi-stopwatch"></i> @Model.Count() round(s)</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@if (Model.Any(r => r.RoundType == RoundType.Final))
|
||||||
|
{
|
||||||
|
<form asp-action="CalculateFinalStandings" method="post">
|
||||||
|
<input type="hidden" name="tournamentEventLevelId" value="@telId" />
|
||||||
|
<button type="submit" class="btn btn-warning" title="Rank the final-round times and award placement points">
|
||||||
|
<i class="bi bi-trophy"></i> Calculate Final Standings
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<partial name="_Notification" />
|
<partial name="_Notification" />
|
||||||
@@ -18,31 +34,50 @@
|
|||||||
<div class="card shadow-sm mb-3">
|
<div class="card shadow-sm mb-3">
|
||||||
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
|
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
|
||||||
<h5 class="mb-0">Rounds</h5>
|
<h5 class="mb-0">Rounds</h5>
|
||||||
<form asp-action="CreateRound" method="post" class="d-flex gap-2">
|
<form asp-action="CreateRound" method="post" class="d-flex gap-2 align-items-center">
|
||||||
<input type="hidden" name="TournamentEventLevelId" value="@telId" />
|
<input type="hidden" name="TournamentEventLevelId" value="@telId" />
|
||||||
<select name="RoundType" class="form-select form-select-sm" style="width:120px">
|
<select name="RoundType" id="roundTypeSelect" class="form-select form-select-sm" style="width:120px">
|
||||||
<option value="Heats">Heats</option>
|
<option value="Heat">Heats</option>
|
||||||
<option value="SemiFinal">Semi-Final</option>
|
<option value="SemiFinal">Semi-Final</option>
|
||||||
<option value="Final">Final</option>
|
<option value="Final">Final</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="number" name="RoundOrder" value="@(Model.Count() + 1)" class="form-control form-control-sm" style="width:60px" />
|
<input type="number" name="RoundOrder" value="@(Model.Count() + 1)" class="form-control form-control-sm" style="width:60px" title="Round order" />
|
||||||
<input type="number" name="AdvanceTopN" value="3" class="form-control form-control-sm" style="width:60px" placeholder="Top N" />
|
<input type="number" name="AdvanceTopN" id="advanceTopN" value="3" class="form-control form-control-sm advance-field" style="width:60px" placeholder="Top N" title="Advance top N per heat" />
|
||||||
<input type="number" name="AdvanceFastestLosers" value="2" class="form-control form-control-sm" style="width:60px" placeholder="FL" />
|
<input type="number" name="AdvanceFastestLosers" id="advanceFL" value="2" class="form-control form-control-sm advance-field" style="width:60px" placeholder="FL" title="Fastest losers" />
|
||||||
|
<span id="finalNote" class="text-light small fst-italic" style="display:none;">no advancement</span>
|
||||||
<button type="submit" class="btn btn-sm btn-success">Add Round</button>
|
<button type="submit" class="btn btn-sm btn-success">Add Round</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
<thead><tr><th>Round</th><th>Type</th><th>Status</th><th>Heats</th><th>Advance</th><th>Actions</th></tr></thead>
|
<thead><tr><th>Round</th><th>Type</th><th>Status</th><th>Heats</th><th>Competitors</th><th>Advances to next round</th><th>Actions</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var r in Model.OrderBy(r => r.RoundOrder))
|
@foreach (var r in Model.OrderBy(r => r.RoundOrder))
|
||||||
{
|
{
|
||||||
|
var competitors = r.Heats.Sum(h => h.HeatLanes.Count);
|
||||||
|
var isFinal = r.RoundType == RoundType.Final;
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>Round @r.RoundOrder</strong></td>
|
<td><strong>Round @r.RoundOrder</strong></td>
|
||||||
<td><span class="badge bg-info">@r.RoundType</span></td>
|
<td><span class="badge bg-info">@r.RoundType</span></td>
|
||||||
<td><span class="badge @(r.Status == RoundStatus.Completed ? "bg-success" : r.Status == RoundStatus.InProgress ? "bg-primary" : "bg-secondary")">@r.Status</span></td>
|
<td><span class="badge @(r.Status == RoundStatus.Completed ? "bg-success" : r.Status == RoundStatus.InProgress ? "bg-primary" : "bg-secondary")">@r.Status</span></td>
|
||||||
<td>@r.Heats.Count heats</td>
|
<td>@r.Heats.Count</td>
|
||||||
<td>Top @(r.AdvanceTopN ?? 0) + @(r.AdvanceFastestLosers ?? 0) FL</td>
|
<td>@competitors</td>
|
||||||
|
<td>
|
||||||
|
@if (isFinal)
|
||||||
|
{
|
||||||
|
<span class="badge bg-warning text-dark">Final — points awarded</span>
|
||||||
|
}
|
||||||
|
else if ((r.AdvanceTopN ?? 0) > 0 || (r.AdvanceFastestLosers ?? 0) > 0)
|
||||||
|
{
|
||||||
|
<span title="Top @(r.AdvanceTopN ?? 0) per heat plus @(r.AdvanceFastestLosers ?? 0) fastest losers overall">
|
||||||
|
Top @(r.AdvanceTopN ?? 0)/heat + @(r.AdvanceFastestLosers ?? 0) fastest losers
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a asp-action="ManageRound" asp-route-roundId="@r.RoundId" class="btn btn-sm btn-primary">Manage</a>
|
<a asp-action="ManageRound" asp-route-roundId="@r.RoundId" class="btn btn-sm btn-primary">Manage</a>
|
||||||
@if (r.Heats.Count == 0)
|
@if (r.Heats.Count == 0)
|
||||||
@@ -59,4 +94,63 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-footer text-muted small">
|
||||||
|
Athletes run preliminary heats (max 8 per heat); the fastest advance to the Final.
|
||||||
|
<strong>Placement points are awarded from the Final round only.</strong>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (standings.Any())
|
||||||
|
{
|
||||||
|
<div class="card shadow-sm mb-3">
|
||||||
|
<div class="card-header text-white" style="background-color:#003366;">
|
||||||
|
<h5 class="mb-0">Final Standings</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<table class="table table-striped table-hover mb-0">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr><th>Place</th><th>Athlete</th><th>School</th><th>Time (s)</th><th>Points</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var r in standings)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
@if (r.Score!.Placement <= 3) { <span class="badge bg-warning text-dark">#@r.Score.Placement</span> }
|
||||||
|
else { <span>@r.Score.Placement</span> }
|
||||||
|
</td>
|
||||||
|
<td>@r.StudentName</td>
|
||||||
|
<td>@r.SchoolName</td>
|
||||||
|
<td>@r.Score.RawPerformance.ToString("0.00")</td>
|
||||||
|
<td><strong>@r.Score.PlacementPoints</strong></td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var sel = document.getElementById('roundTypeSelect');
|
||||||
|
if (!sel) return;
|
||||||
|
var topN = document.getElementById('advanceTopN');
|
||||||
|
var fl = document.getElementById('advanceFL');
|
||||||
|
var note = document.getElementById('finalNote');
|
||||||
|
function sync() {
|
||||||
|
var isFinal = sel.value === 'Final';
|
||||||
|
// Finals don't advance anyone, so hide the advancement inputs.
|
||||||
|
[topN, fl].forEach(function (el) {
|
||||||
|
if (!el) return;
|
||||||
|
el.style.display = isFinal ? 'none' : '';
|
||||||
|
el.disabled = isFinal;
|
||||||
|
});
|
||||||
|
if (note) note.style.display = isFinal ? '' : 'none';
|
||||||
|
}
|
||||||
|
sel.addEventListener('change', sync);
|
||||||
|
sync();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,12 +16,19 @@
|
|||||||
<input type="hidden" name="roundId" value="@Model.RoundId" />
|
<input type="hidden" name="roundId" value="@Model.RoundId" />
|
||||||
<button type="submit" class="btn btn-warning" onclick="return confirm('Calculate advancement?')">Calculate Advancement</button>
|
<button type="submit" class="btn btn-warning" onclick="return confirm('Calculate advancement?')">Calculate Advancement</button>
|
||||||
</form>
|
</form>
|
||||||
|
@if (ViewBag.HasNextRound == true)
|
||||||
|
{
|
||||||
|
<form asp-action="PopulateNextRound" method="post">
|
||||||
|
<input type="hidden" name="roundId" value="@Model.RoundId" />
|
||||||
|
<button type="submit" class="btn btn-info" onclick="return confirm('Move the advancing athletes into the next round?')">Send Advancers to Next Round</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
<form asp-action="CompleteRound" method="post">
|
<form asp-action="CompleteRound" method="post">
|
||||||
<input type="hidden" name="roundId" value="@Model.RoundId" />
|
<input type="hidden" name="roundId" value="@Model.RoundId" />
|
||||||
<button type="submit" class="btn btn-success" onclick="return confirm('Complete this round?')">Complete Round</button>
|
<button type="submit" class="btn btn-success" onclick="return confirm('Complete this round?')">Complete Round</button>
|
||||||
</form>
|
</form>
|
||||||
}
|
}
|
||||||
<a asp-action="Index" asp-route-tournamentEventLevelId="@telId" class="btn btn-secondary">Back</a>
|
<a asp-action="Index" asp-route-tournamentEventLevelId="@Model.TournamentEventLevelId" class="btn btn-secondary">Back</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -54,6 +61,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form asp-action="SaveTimes" method="post">
|
<form asp-action="SaveTimes" method="post">
|
||||||
<input type="hidden" name="heatId" value="@heat.HeatId" />
|
<input type="hidden" name="heatId" value="@heat.HeatId" />
|
||||||
|
<input type="hidden" name="roundId" value="@Model.RoundId" />
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
355
test-data.sql
Normal file
355
test-data.sql
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- test-data.sql
|
||||||
|
-- Bulk sample data for the Dominica Sports Division app.
|
||||||
|
--
|
||||||
|
-- Adds, for BOTH seeded tournaments (1 = Inter-Zone Championship 2026,
|
||||||
|
-- 2 = National Championship 2026):
|
||||||
|
-- * 60 students (30 male / 30 female) spread across the secondary schools
|
||||||
|
-- * Under-16 Boys & Under-16 Girls event levels for EVERY individual
|
||||||
|
-- (non-relay) event in the catalogue
|
||||||
|
-- * ~16 registrations per event level
|
||||||
|
-- * Results recorded in the form each discipline actually uses:
|
||||||
|
-- - Field events: a Score per athlete (mark -> WA points, placement, points)
|
||||||
|
-- - High jump: a per-bar attempt grid (clears / fails), then a Score
|
||||||
|
-- - Track: a preliminary Heat round (heats of <= 8) feeding an
|
||||||
|
-- 8-athlete Final; placement points are awarded ONLY from
|
||||||
|
-- the final.
|
||||||
|
--
|
||||||
|
-- Marks are VARIED per event level (a deterministic per-registration jitter and
|
||||||
|
-- a per-level base offset) so no two competitions look identical.
|
||||||
|
--
|
||||||
|
-- The students / event levels / registrations are inserted idempotently. The
|
||||||
|
-- scoring artifacts (Scores, heats, heights, attempts) are rebuilt on every run
|
||||||
|
-- (deterministically), so re-running reproduces the same data -- but any manual
|
||||||
|
-- scoring done on these U16 test event levels via the UI will be overwritten.
|
||||||
|
--
|
||||||
|
-- Field / jump marks are in METRES. Relays are skipped (they need RelayTeams).
|
||||||
|
--
|
||||||
|
-- Run with:
|
||||||
|
-- PGPASSWORD=... psql -h localhost -U postgres -d sportsdivision -f test-data.sql
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Single source of truth: every individual event to populate, with the base
|
||||||
|
-- mark for the winner and the step between consecutive places.
|
||||||
|
-- is_track = true -> seconds, lower is better
|
||||||
|
-- is_track = false -> metres, higher is better
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
CREATE TEMP TABLE _ev (ename text, base numeric, step numeric, is_track boolean) ON COMMIT DROP;
|
||||||
|
INSERT INTO _ev (ename, base, step, is_track) VALUES
|
||||||
|
-- Track (seconds)
|
||||||
|
('80m', 10.00, 0.15, true),
|
||||||
|
('100m', 11.50, 0.18, true),
|
||||||
|
('150m', 17.50, 0.25, true),
|
||||||
|
('200m', 23.50, 0.30, true),
|
||||||
|
('300m', 38.00, 0.45, true),
|
||||||
|
('400m', 52.00, 0.70, true),
|
||||||
|
('800m', 125.00, 1.50, true),
|
||||||
|
('1200m', 200.00, 2.50, true),
|
||||||
|
('1500m', 255.00, 3.00, true),
|
||||||
|
('3000m', 560.00, 6.00, true),
|
||||||
|
('5000m', 980.00, 10.0, true),
|
||||||
|
('80mH', 13.00, 0.25, true),
|
||||||
|
('QC 60m Sprint', 7.50, 0.12, true),
|
||||||
|
-- Field (metres)
|
||||||
|
('Long Jump', 6.40, 0.15, false),
|
||||||
|
('Triple Jump', 13.00, 0.30, false),
|
||||||
|
('Shot Put 3kg', 12.50, 0.40, false),
|
||||||
|
('Shot Put 4kg', 11.50, 0.38, false),
|
||||||
|
('Shot Put 5kg', 10.50, 0.35, false),
|
||||||
|
('Shot Put 6kg', 9.50, 0.33, false),
|
||||||
|
('Discus 1kg', 38.00, 1.20, false),
|
||||||
|
('Discus 1.25kg', 34.00, 1.10, false),
|
||||||
|
('Discus 1.5kg', 30.00, 1.00, false),
|
||||||
|
('Discus 1.75kg', 27.00, 0.90, false),
|
||||||
|
('Javelin 400g', 42.00, 1.50, false),
|
||||||
|
('Javelin 500g', 48.00, 1.60, false),
|
||||||
|
('Javelin 600g', 45.00, 1.50, false),
|
||||||
|
('Javelin 700g', 40.00, 1.40, false),
|
||||||
|
('Javelin 800g', 38.00, 1.30, false),
|
||||||
|
('Throwing the Cricket Ball', 65.00, 2.00, false),
|
||||||
|
('QC Javelin Throw', 40.00, 1.40, false),
|
||||||
|
-- High jump (metres)
|
||||||
|
('High Jump', 1.85, 0.04, false),
|
||||||
|
('QC High Jump Open', 1.80, 0.04, false);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 1. STUDENTS (60: TD-0001 .. TD-0060; first 30 Male, next 30 Female)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
WITH secids AS (
|
||||||
|
SELECT array_agg("SchoolId" ORDER BY "SchoolId") AS ids, count(*)::int AS c
|
||||||
|
FROM "Schools"
|
||||||
|
WHERE "SchoolLevel" = 'Secondary' AND "IsActive"
|
||||||
|
)
|
||||||
|
INSERT INTO "Students"
|
||||||
|
("ExistingStudentId", "FirstName", "LastName", "DateOfBirth", "Sex", "SchoolId", "IsActive")
|
||||||
|
SELECT
|
||||||
|
'TD-' || lpad(g::text, 4, '0'),
|
||||||
|
(ARRAY['Liam','Noah','Ethan','Mason','Logan','Lucas','Jack','Aiden','Caleb','Isaac',
|
||||||
|
'Ava','Mia','Zoe','Lily','Emma','Sofia','Maya','Chloe','Ella','Grace'])[1 + (g % 20)],
|
||||||
|
(ARRAY['Joseph','Charles','Baptiste','Pierre','Williams','John','Thomas','James','Henry','George',
|
||||||
|
'Edwards','Francis','Lewis','Daniel','Peters','Roberts','Andrew','Paul','Mark','Joseph'])[1 + ((g * 7) % 20)],
|
||||||
|
make_date(2010 + (g % 2), 1 + (g % 12), 1 + (g % 27)),
|
||||||
|
CASE WHEN g <= 30 THEN 'Male' ELSE 'Female' END,
|
||||||
|
secids.ids[1 + (g % secids.c)],
|
||||||
|
true
|
||||||
|
FROM generate_series(1, 60) AS g
|
||||||
|
CROSS JOIN secids
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM "Students" s WHERE s."ExistingStudentId" = 'TD-' || lpad(g::text, 4, '0')
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 2. TOURNAMENT EVENT LEVELS (every _ev event x U16 Boys/Girls x 2 tournaments)
|
||||||
|
-- Age restriction waived so the U16 sample athletes are always eligible.
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
WITH combos AS (
|
||||||
|
SELECT t.tid, e."EventId" AS eid, el."EventLevelId" AS lid
|
||||||
|
FROM (VALUES (1), (2)) AS t(tid)
|
||||||
|
CROSS JOIN _ev
|
||||||
|
CROSS JOIN (VALUES ('Under 16 Boys'), ('Under 16 Girls')) AS lv(name)
|
||||||
|
JOIN "Events" e ON e."Name" = _ev.ename
|
||||||
|
JOIN "EventLevels" el ON el."Name" = lv.name
|
||||||
|
)
|
||||||
|
INSERT INTO "TournamentEventLevels"
|
||||||
|
("TournamentId", "EventId", "EventLevelId", "AgeRestrictionWaived")
|
||||||
|
SELECT c.tid, c.eid, c.lid, true
|
||||||
|
FROM combos c
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM "TournamentEventLevels" x
|
||||||
|
WHERE x."TournamentId" = c.tid AND x."EventId" = c.eid AND x."EventLevelId" = c.lid
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 3. REGISTRATIONS (~16 sample students per event level, sex-matched)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
WITH testtels AS (
|
||||||
|
SELECT tel."TournamentEventLevelId" AS tid, el."Sex" AS sex
|
||||||
|
FROM "TournamentEventLevels" tel
|
||||||
|
JOIN "EventLevels" el ON el."EventLevelId" = tel."EventLevelId"
|
||||||
|
JOIN "Events" e ON e."EventId" = tel."EventId"
|
||||||
|
WHERE tel."TournamentId" IN (1, 2)
|
||||||
|
AND el."Name" IN ('Under 16 Boys', 'Under 16 Girls')
|
||||||
|
AND e."Name" IN (SELECT ename FROM _ev)
|
||||||
|
),
|
||||||
|
studs AS (
|
||||||
|
SELECT "StudentId", "Sex",
|
||||||
|
row_number() OVER (PARTITION BY "Sex" ORDER BY "StudentId") AS rn
|
||||||
|
FROM "Students"
|
||||||
|
WHERE "ExistingStudentId" LIKE 'TD-%'
|
||||||
|
)
|
||||||
|
INSERT INTO "EventRegistrations"
|
||||||
|
("TournamentEventLevelId", "StudentId", "RegisteredBy", "RegisteredAt")
|
||||||
|
SELECT t.tid, s."StudentId", 'seed@sportsdivision.dm', TIMESTAMP WITH TIME ZONE '2026-06-01 09:00:00+00'
|
||||||
|
FROM testtels t
|
||||||
|
JOIN studs s ON s."Sex" = t.sex
|
||||||
|
WHERE ((s.rn + t.tid) % 30) < 16 -- 16 of the 30 per level, varied per TEL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM "EventRegistrations" r
|
||||||
|
WHERE r."TournamentEventLevelId" = t.tid AND r."StudentId" = s."StudentId"
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ===========================================================================
|
||||||
|
-- SCORING (rebuilt from scratch each run for the U16 test event levels)
|
||||||
|
-- ===========================================================================
|
||||||
|
|
||||||
|
-- The test event levels we own (U16 Boys/Girls, both tournaments, _ev events).
|
||||||
|
CREATE TEMP TABLE _tel ON COMMIT DROP AS
|
||||||
|
SELECT tel."TournamentEventLevelId" AS tid, e."EventId" AS eid, e."Name" AS ename, e."Category" AS cat
|
||||||
|
FROM "TournamentEventLevels" tel
|
||||||
|
JOIN "EventLevels" el ON el."EventLevelId" = tel."EventLevelId"
|
||||||
|
JOIN "Events" e ON e."EventId" = tel."EventId"
|
||||||
|
WHERE tel."TournamentId" IN (1, 2)
|
||||||
|
AND el."Name" IN ('Under 16 Boys', 'Under 16 Girls')
|
||||||
|
AND e."Name" IN (SELECT ename FROM _ev);
|
||||||
|
|
||||||
|
-- Clear any existing scoring artifacts for those event levels (FK-safe order).
|
||||||
|
DELETE FROM "HeatLanes" WHERE "HeatId" IN (
|
||||||
|
SELECT h."HeatId" FROM "Heats" h JOIN "Rounds" r ON r."RoundId" = h."RoundId"
|
||||||
|
WHERE r."TournamentEventLevelId" IN (SELECT tid FROM _tel));
|
||||||
|
DELETE FROM "Heats" WHERE "RoundId" IN (
|
||||||
|
SELECT "RoundId" FROM "Rounds" WHERE "TournamentEventLevelId" IN (SELECT tid FROM _tel));
|
||||||
|
DELETE FROM "Rounds" WHERE "TournamentEventLevelId" IN (SELECT tid FROM _tel);
|
||||||
|
DELETE FROM "HighJumpAttempts" WHERE "HighJumpHeightId" IN (
|
||||||
|
SELECT "HighJumpHeightId" FROM "HighJumpHeights" WHERE "TournamentEventLevelId" IN (SELECT tid FROM _tel));
|
||||||
|
DELETE FROM "HighJumpHeights" WHERE "TournamentEventLevelId" IN (SELECT tid FROM _tel);
|
||||||
|
DELETE FROM "Scores" WHERE "EventRegistrationId" IN (
|
||||||
|
SELECT er."EventRegistrationId" FROM "EventRegistrations" er WHERE er."TournamentEventLevelId" IN (SELECT tid FROM _tel));
|
||||||
|
|
||||||
|
-- Per-athlete performance, VARIED: a per-level base offset (+/-6% by tid) and a
|
||||||
|
-- deterministic per-registration jitter shuffle finishing order and marks, so
|
||||||
|
-- different athletes win different events and no two competitions are identical.
|
||||||
|
CREATE TEMP TABLE _perf ON COMMIT DROP AS
|
||||||
|
WITH b AS (
|
||||||
|
SELECT er."EventRegistrationId" AS erid, t.tid, t.eid, t.ename, t.cat,
|
||||||
|
p.base, p.step, p.is_track,
|
||||||
|
(p.base * (1 + (((t.tid % 7) - 3) * 0.02))) AS base_v,
|
||||||
|
((er."EventRegistrationId"::bigint * 48271) % 100000) AS seed,
|
||||||
|
((((er."EventRegistrationId"::bigint * 1103515245 + 12345) % 1000)::numeric / 1000.0) - 0.5) AS jit
|
||||||
|
FROM _tel t
|
||||||
|
JOIN "EventRegistrations" er ON er."TournamentEventLevelId" = t.tid AND er."StudentId" IS NOT NULL
|
||||||
|
JOIN _ev p ON p.ename = t.ename
|
||||||
|
),
|
||||||
|
o AS (SELECT *, row_number() OVER (PARTITION BY tid ORDER BY seed) AS pos0 FROM b),
|
||||||
|
m AS (
|
||||||
|
SELECT *,
|
||||||
|
CASE WHEN is_track THEN base_v + step * (pos0 - 1) + jit * step
|
||||||
|
ELSE base_v - step * (pos0 - 1) + jit * step END AS raw0
|
||||||
|
FROM o
|
||||||
|
)
|
||||||
|
SELECT erid, tid, eid, ename, cat, is_track, jit,
|
||||||
|
round(raw0::numeric, 2) AS raw,
|
||||||
|
row_number() OVER (PARTITION BY tid ORDER BY CASE WHEN is_track THEN raw0 ELSE -raw0 END) AS pos,
|
||||||
|
count(*) OVER (PARTITION BY tid) AS n
|
||||||
|
FROM m;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 4a. FIELD events (single competition; everyone placed, top 8 score points)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
INSERT INTO "Scores"
|
||||||
|
("EventRegistrationId", "RawPerformance", "CalculatedPoints", "Placement", "PlacementPoints", "RecordedBy", "RecordedAt")
|
||||||
|
SELECT pf.erid, pf.raw,
|
||||||
|
COALESCE(CASE WHEN sc."A" IS NOT NULL AND (pf.raw - sc."B") > 0
|
||||||
|
THEN floor(sc."A" * power(pf.raw - sc."B", sc."C"))::int ELSE 0 END, 0),
|
||||||
|
pf.pos, COALESCE(ppc."Points", 0),
|
||||||
|
'seed@sportsdivision.dm', TIMESTAMP WITH TIME ZONE '2026-06-02 14:00:00+00'
|
||||||
|
FROM _perf pf
|
||||||
|
LEFT JOIN "ScoringConstants" sc ON sc."EventId" = pf.eid
|
||||||
|
LEFT JOIN "PlacementPointConfigs" ppc ON ppc."Placement" = pf.pos
|
||||||
|
WHERE pf.cat = 'Field' AND pf.raw > 0;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 4b. HIGH JUMP (per-level bar ladder, jittered clearances with ties)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
CREATE TEMP TABLE _hj ON COMMIT DROP AS
|
||||||
|
SELECT pf.erid, pf.tid, pf.eid, pf.pos,
|
||||||
|
(1.30 + (pf.tid % 6) * 0.05)::numeric AS start_h,
|
||||||
|
GREATEST(0, LEAST(7,
|
||||||
|
(round((pf.n - pf.pos)::numeric / pf.n * 7) + round(pf.jit * 2))::int)) AS cidx
|
||||||
|
FROM _perf pf
|
||||||
|
WHERE pf.cat = 'HighJump';
|
||||||
|
|
||||||
|
-- bar ladder: 8 bars at 0.05 spacing from the per-level start height
|
||||||
|
INSERT INTO "HighJumpHeights" ("TournamentEventLevelId", "Height", "SortOrder")
|
||||||
|
SELECT s.tid, round((s.start_h + 0.05 * k)::numeric, 2), k + 1
|
||||||
|
FROM (SELECT DISTINCT tid, start_h FROM _hj) s
|
||||||
|
CROSS JOIN generate_series(0, 7) AS k;
|
||||||
|
|
||||||
|
-- attempts: clear every bar up to the athlete's height, fail (XXX) the next one
|
||||||
|
INSERT INTO "HighJumpAttempts" ("HighJumpHeightId", "EventRegistrationId", "Attempt1", "Attempt2", "Attempt3")
|
||||||
|
SELECT hh."HighJumpHeightId", a.erid,
|
||||||
|
CASE WHEN hh."Height" <= a.cleared THEN 'Clear' ELSE 'Fail' END,
|
||||||
|
CASE WHEN hh."Height" > a.cleared THEN 'Fail' END,
|
||||||
|
CASE WHEN hh."Height" > a.cleared THEN 'Fail' END
|
||||||
|
FROM (SELECT erid, tid, round((start_h + 0.05 * cidx)::numeric, 2) AS cleared FROM _hj) a
|
||||||
|
JOIN "HighJumpHeights" hh
|
||||||
|
ON hh."TournamentEventLevelId" = a.tid
|
||||||
|
AND hh."Height" <= a.cleared + 0.05;
|
||||||
|
|
||||||
|
-- high jump scores: rank by cleared height (ties broken by finishing order)
|
||||||
|
INSERT INTO "Scores"
|
||||||
|
("EventRegistrationId", "RawPerformance", "CalculatedPoints", "Placement", "PlacementPoints", "RecordedBy", "RecordedAt")
|
||||||
|
SELECT z.erid, z.cleared,
|
||||||
|
COALESCE(CASE WHEN sc."A" IS NOT NULL AND (z.cleared - sc."B") > 0
|
||||||
|
THEN floor(sc."A" * power(z.cleared - sc."B", sc."C"))::int ELSE 0 END, 0),
|
||||||
|
z.place, COALESCE(ppc."Points", 0),
|
||||||
|
'seed@sportsdivision.dm', TIMESTAMP WITH TIME ZONE '2026-06-02 14:00:00+00'
|
||||||
|
FROM (
|
||||||
|
SELECT h.erid, h.eid, round((h.start_h + 0.05 * h.cidx)::numeric, 2) AS cleared,
|
||||||
|
row_number() OVER (PARTITION BY h.tid ORDER BY (h.start_h + 0.05 * h.cidx) DESC, h.pos ASC) AS place
|
||||||
|
FROM _hj h
|
||||||
|
) z
|
||||||
|
LEFT JOIN "ScoringConstants" sc ON sc."EventId" = z.eid
|
||||||
|
LEFT JOIN "PlacementPointConfigs" ppc ON ppc."Placement" = z.place;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- 4c. TRACK (prelim Heat round of <=8-lane heats -> 8-athlete Final)
|
||||||
|
-- Placement points are awarded only from the Final round.
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
CREATE TEMP TABLE _trk ON COMMIT DROP AS
|
||||||
|
SELECT pf.erid, pf.tid, pf.eid, pf.pos, pf.n,
|
||||||
|
pf.raw AS prelim_time,
|
||||||
|
round((pf.raw * 0.985)::numeric, 2) AS final_time,
|
||||||
|
ceil(pf.n / 8.0)::int AS heats
|
||||||
|
FROM _perf pf
|
||||||
|
WHERE pf.cat = 'Track';
|
||||||
|
|
||||||
|
-- prelim round (Top 3 per heat + 2 fastest losers advance to the final)
|
||||||
|
INSERT INTO "Rounds" ("TournamentEventLevelId", "RoundType", "RoundOrder", "AdvanceTopN", "AdvanceFastestLosers", "Status")
|
||||||
|
SELECT DISTINCT tid, 'Heat', 1, 3, 2, 'Completed' FROM _trk;
|
||||||
|
-- final round
|
||||||
|
INSERT INTO "Rounds" ("TournamentEventLevelId", "RoundType", "RoundOrder", "AdvanceTopN", "AdvanceFastestLosers", "Status")
|
||||||
|
SELECT DISTINCT tid, 'Final', 2, NULL::int, NULL::int, 'Completed' FROM _trk;
|
||||||
|
|
||||||
|
-- prelim heats: ceil(n/8) heats so no heat exceeds 8 lanes
|
||||||
|
INSERT INTO "Heats" ("RoundId", "HeatNumber", "Status")
|
||||||
|
SELECT r."RoundId", hn.n, 'Completed'
|
||||||
|
FROM "Rounds" r
|
||||||
|
JOIN (SELECT DISTINCT tid, heats FROM _trk) d ON d.tid = r."TournamentEventLevelId"
|
||||||
|
CROSS JOIN LATERAL generate_series(1, d.heats) AS hn(n)
|
||||||
|
WHERE r."RoundOrder" = 1;
|
||||||
|
-- final heat (single heat of <= 8)
|
||||||
|
INSERT INTO "Heats" ("RoundId", "HeatNumber", "Status")
|
||||||
|
SELECT r."RoundId", 1, 'Completed'
|
||||||
|
FROM "Rounds" r
|
||||||
|
JOIN (SELECT DISTINCT tid FROM _trk) d ON d.tid = r."TournamentEventLevelId"
|
||||||
|
WHERE r."RoundOrder" = 2;
|
||||||
|
|
||||||
|
-- prelim lanes: round-robin athletes into the heats; fastest 8 overall advance
|
||||||
|
INSERT INTO "HeatLanes"
|
||||||
|
("HeatId", "EventRegistrationId", "LaneNumber", "Time", "IsAdvanced", "AdvanceReason",
|
||||||
|
"IsDNS", "IsDNF", "IsDQ", "RecordedBy", "RecordedAt")
|
||||||
|
SELECT h."HeatId", t.erid,
|
||||||
|
row_number() OVER (PARTITION BY h."HeatId" ORDER BY t.prelim_time),
|
||||||
|
t.prelim_time,
|
||||||
|
(t.pos <= 8),
|
||||||
|
CASE WHEN t.pos <= 6 THEN 'TopN' WHEN t.pos <= 8 THEN 'FastestLoser' ELSE NULL END,
|
||||||
|
false, false, false, 'seed@sportsdivision.dm', TIMESTAMP WITH TIME ZONE '2026-06-02 14:00:00+00'
|
||||||
|
FROM _trk t
|
||||||
|
JOIN "Rounds" r ON r."TournamentEventLevelId" = t.tid AND r."RoundOrder" = 1
|
||||||
|
JOIN "Heats" h ON h."RoundId" = r."RoundId" AND h."HeatNumber" = ((t.pos - 1) % t.heats) + 1;
|
||||||
|
|
||||||
|
-- final lanes: the 8 finalists, lane = final placement
|
||||||
|
INSERT INTO "HeatLanes"
|
||||||
|
("HeatId", "EventRegistrationId", "LaneNumber", "Time", "IsAdvanced", "AdvanceReason",
|
||||||
|
"IsDNS", "IsDNF", "IsDQ", "RecordedBy", "RecordedAt")
|
||||||
|
SELECT h."HeatId", t.erid, t.pos, t.final_time, false, NULL,
|
||||||
|
false, false, false, 'seed@sportsdivision.dm', TIMESTAMP WITH TIME ZONE '2026-06-02 14:00:00+00'
|
||||||
|
FROM _trk t
|
||||||
|
JOIN "Rounds" r ON r."TournamentEventLevelId" = t.tid AND r."RoundOrder" = 2
|
||||||
|
JOIN "Heats" h ON h."RoundId" = r."RoundId"
|
||||||
|
WHERE t.pos <= 8;
|
||||||
|
|
||||||
|
-- track scores: ONLY the 8 finalists, placement & points from the final
|
||||||
|
INSERT INTO "Scores"
|
||||||
|
("EventRegistrationId", "RawPerformance", "CalculatedPoints", "Placement", "PlacementPoints", "RecordedBy", "RecordedAt")
|
||||||
|
SELECT t.erid, t.final_time,
|
||||||
|
COALESCE(CASE WHEN sc."A" IS NOT NULL AND (sc."B" - t.final_time) > 0
|
||||||
|
THEN floor(sc."A" * power(sc."B" - t.final_time, sc."C"))::int ELSE 0 END, 0),
|
||||||
|
t.pos, COALESCE(ppc."Points", 0),
|
||||||
|
'seed@sportsdivision.dm', TIMESTAMP WITH TIME ZONE '2026-06-02 14:00:00+00'
|
||||||
|
FROM _trk t
|
||||||
|
LEFT JOIN "ScoringConstants" sc ON sc."EventId" = t.eid
|
||||||
|
LEFT JOIN "PlacementPointConfigs" ppc ON ppc."Placement" = t.pos
|
||||||
|
WHERE t.pos <= 8;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Summary (informational; safe to run repeatedly)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
SELECT 'students (TD-)' AS metric, count(*) AS value FROM "Students" WHERE "ExistingStudentId" LIKE 'TD-%'
|
||||||
|
UNION ALL SELECT 'event levels (t1)', count(*) FROM "TournamentEventLevels" WHERE "TournamentId" = 1
|
||||||
|
UNION ALL SELECT 'event levels (t2)', count(*) FROM "TournamentEventLevels" WHERE "TournamentId" = 2
|
||||||
|
UNION ALL SELECT 'registrations (total)', count(*) FROM "EventRegistrations"
|
||||||
|
UNION ALL SELECT 'scores (total)', count(*) FROM "Scores"
|
||||||
|
UNION ALL SELECT 'track heat lanes', count(*) FROM "HeatLanes"
|
||||||
|
UNION ALL SELECT 'high jump heights', count(*) FROM "HighJumpHeights"
|
||||||
|
UNION ALL SELECT 'high jump attempts', count(*) FROM "HighJumpAttempts"
|
||||||
|
UNION ALL SELECT 'individual events scored',
|
||||||
|
count(DISTINCT tel."EventId")
|
||||||
|
FROM "Scores" s
|
||||||
|
JOIN "EventRegistrations" er ON er."EventRegistrationId" = s."EventRegistrationId"
|
||||||
|
JOIN "TournamentEventLevels" tel ON tel."TournamentEventLevelId" = er."TournamentEventLevelId";
|
||||||
Reference in New Issue
Block a user