dashboard upgrade

This commit is contained in:
2026-06-27 14:19:17 -04:00
parent ddb2d58e2f
commit 2d9a5b52b7
8 changed files with 795 additions and 65 deletions

View File

@@ -1,4 +1,4 @@
namespace SportsDivision.Application.DTOs;
public class DashboardDto { public int TotalStudents { get; set; } public int MaleStudents { get; set; } public int FemaleStudents { get; set; } public int TotalSchools { get; set; } public int ActiveTournaments { get; set; } public int TotalRegistrations { get; set; } public int EventsInProgress { get; set; } public int EventsCompleted { get; set; } public List<SchoolPointsSummaryDto> TopSchools { get; set; } = new(); public List<RecentScoreDto> RecentScores { get; set; } = new(); }
public class DashboardDto { public int TotalStudents { get; set; } public int MaleStudents { get; set; } public int FemaleStudents { get; set; } public int TotalSchools { get; set; } public int TotalEvents { get; set; } public int TotalScores { get; set; } public int ActiveTournaments { get; set; } public int DraftTournaments { get; set; } public int RegistrationTournaments { get; set; } public int CompletedTournaments { get; set; } public int ArchivedTournaments { get; set; } public int TotalRegistrations { get; set; } public int EventsInProgress { get; set; } public int EventsCompleted { get; set; } public List<SchoolPointsSummaryDto> TopSchools { get; set; } = new(); public List<RecentScoreDto> RecentScores { get; set; } = new(); }
public class SchoolPointsSummaryDto { public int SchoolId { get; set; } public string SchoolName { get; set; } = string.Empty; public string? ShortName { get; set; } public int TotalPoints { get; set; } public int FirstPlaceCount { get; set; } public int SecondPlaceCount { get; set; } public int ThirdPlaceCount { get; set; } }
public class RecentScoreDto { public string StudentName { get; set; } = string.Empty; public string EventName { get; set; } = string.Empty; public string EventLevelName { get; set; } = string.Empty; public decimal RawPerformance { get; set; } public int? Placement { get; set; } public DateTime RecordedAt { get; set; } }

View File

@@ -24,19 +24,48 @@ public class DashboardService : IDashboardService
MaleStudents = await _uow.Students.CountAsync(s => s.Sex == Sex.Male),
FemaleStudents = await _uow.Students.CountAsync(s => s.Sex == Sex.Female),
TotalSchools = await _uow.Schools.CountAsync(),
ActiveTournaments = await _uow.Tournaments.CountAsync(t => t.Status == TournamentStatus.InProgress && !t.IsArchived)
TotalEvents = await _uow.Events.CountAsync(),
TotalScores = await _uow.Scores.CountAsync(),
ActiveTournaments = await _uow.Tournaments.CountAsync(t => t.Status == TournamentStatus.InProgress && !t.IsArchived),
DraftTournaments = await _uow.Tournaments.CountAsync(t => t.Status == TournamentStatus.Draft && !t.IsArchived),
RegistrationTournaments = await _uow.Tournaments.CountAsync(t => t.Status == TournamentStatus.Registration && !t.IsArchived),
CompletedTournaments = await _uow.Tournaments.CountAsync(t => t.Status == TournamentStatus.Completed && !t.IsArchived),
ArchivedTournaments = await _uow.Tournaments.CountAsync(t => t.IsArchived)
};
if (tournamentId.HasValue)
{
var tels = await _uow.TournamentEventLevels.GetByTournamentAsync(tournamentId.Value);
var tels = (await _uow.TournamentEventLevels.GetByTournamentAsync(tournamentId.Value)).ToList();
int regCount = 0;
int scoredEvents = 0;
var recent = new List<RecentScoreDto>();
foreach (var tel in tels)
{
var regs = await _uow.EventRegistrations.GetByTournamentEventLevelAsync(tel.TournamentEventLevelId);
regCount += regs.Count();
bool hasResults = false;
foreach (var reg in regs.Where(r => r.Score != null))
{
if (reg.Score!.Placement != null) hasResults = true;
recent.Add(new RecentScoreDto
{
StudentName = reg.Student?.FullName ?? "Unknown",
EventName = tel.Event?.Name ?? string.Empty,
EventLevelName = tel.EventLevel?.Name ?? string.Empty,
RawPerformance = reg.Score!.RawPerformance,
Placement = reg.Score.Placement,
RecordedAt = reg.Score.RecordedAt
});
}
if (hasResults) scoredEvents++;
}
dashboard.TotalRegistrations = regCount;
dashboard.EventsCompleted = scoredEvents;
dashboard.EventsInProgress = tels.Count - scoredEvents;
dashboard.RecentScores = recent.OrderByDescending(r => r.RecordedAt).Take(8).ToList();
var standings = await _scoringService.GetSchoolStandingsAsync(tournamentId.Value);
dashboard.TopSchools = standings.Take(10).ToList();

View File

@@ -18,11 +18,25 @@ public class DashboardController : Controller
public async Task<IActionResult> Index(int? tournamentId)
{
var dashboard = await _dashboardService.GetDashboardAsync(tournamentId);
// "Active" tournaments are the non-archived ones; archived tournaments are never shown here.
var tournaments = (await _tournamentService.GetAllAsync(includeArchived: false)).ToList();
// Ignore a selection that points at an archived/unknown tournament.
int? selectedId = tournamentId.HasValue && tournaments.Any(t => t.TournamentId == tournamentId.Value)
? tournamentId
: null;
// With exactly one active tournament, show it automatically — no need to choose.
// With several, the user must pick one (selectedId stays null until they do).
if (!selectedId.HasValue && tournaments.Count == 1)
{
selectedId = tournaments[0].TournamentId;
}
var dashboard = await _dashboardService.GetDashboardAsync(selectedId);
var tournaments = await _tournamentService.GetAllAsync(includeArchived: false);
ViewBag.Tournaments = tournaments;
ViewBag.SelectedTournamentId = tournamentId;
ViewBag.SelectedTournamentId = selectedId;
return View(dashboard);
}

View File

@@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace SportsDivision.Web.Controllers;
[Authorize]
public class HelpController : Controller
{
public IActionResult Index() => View();
}

View File

@@ -1,70 +1,171 @@
@model DashboardDto
@{
ViewData["Title"] = "Dashboard";
var tournaments = ViewBag.Tournaments as IEnumerable<TournamentDto>;
var tournamentList = (ViewBag.Tournaments as IEnumerable<TournamentDto>)?.ToList() ?? new List<TournamentDto>();
var selectedTournamentId = ViewBag.SelectedTournamentId as int?;
var selectedTournament = tournamentList.FirstOrDefault(t => t.TournamentId == selectedTournamentId);
}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Dashboard</h2>
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
<h2 class="mb-0">Dashboard</h2>
@if (tournamentList.Count > 1)
{
<form method="get" class="d-flex align-items-center gap-2">
<label class="form-label mb-0 text-nowrap">Tournament:</label>
<select name="tournamentId" class="form-select form-select-sm" style="width:250px" onchange="this.form.submit()">
<option value="">-- All --</option>
@if (tournaments != null)
{
@foreach (var t in tournaments)
<option value="">-- Select a tournament --</option>
@foreach (var t in tournamentList)
{
<option value="@t.TournamentId" selected="@(selectedTournamentId == t.TournamentId)">@t.Name</option>
}
}
</select>
</form>
}
else if (tournamentList.Count == 1)
{
<span class="badge bg-primary fs-6"><i class="bi bi-trophy me-1"></i>@tournamentList[0].Name</span>
}
</div>
@* ---- Top-level counts (system-wide) ---- *@
<div class="row g-3 mb-4">
<div class="col-md-3">
<div class="card bg-primary text-white shadow-sm">
<div class="col-6 col-md-3">
<div class="card bg-primary text-white shadow-sm h-100">
<div class="card-body">
<h6 class="card-title">Total Students</h6>
<h2>@Model.TotalStudents</h2>
<h6 class="card-title"><i class="bi bi-people me-1"></i>Total Students</h6>
<h2 class="mb-0">@Model.TotalStudents</h2>
<small>Male: @Model.MaleStudents | Female: @Model.FemaleStudents</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white shadow-sm">
<div class="col-6 col-md-3">
<div class="card bg-success text-white shadow-sm h-100">
<div class="card-body">
<h6 class="card-title">Schools</h6>
<h2>@Model.TotalSchools</h2>
<h6 class="card-title"><i class="bi bi-building me-1"></i>Schools</h6>
<h2 class="mb-0">@Model.TotalSchools</h2>
<small>Participating institutions</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-info text-white shadow-sm">
<div class="col-6 col-md-3">
<div class="card bg-info text-white shadow-sm h-100">
<div class="card-body">
<h6 class="card-title">Active Tournaments</h6>
<h2>@Model.ActiveTournaments</h2>
<h6 class="card-title"><i class="bi bi-calendar-event me-1"></i>Events</h6>
<h2 class="mb-0">@Model.TotalEvents</h2>
<small>Track, field &amp; high jump</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-warning text-dark shadow-sm">
<div class="col-6 col-md-3">
<div class="card bg-dark text-white shadow-sm h-100">
<div class="card-body">
<h6 class="card-title">Registrations</h6>
<h2>@Model.TotalRegistrations</h2>
<h6 class="card-title"><i class="bi bi-clipboard-check me-1"></i>Scores Recorded</h6>
<h2 class="mb-0">@Model.TotalScores</h2>
<small>Across all tournaments</small>
</div>
</div>
</div>
</div>
@if (Model.TopSchools.Any())
{
<div class="card shadow-sm">
@* ---- Tournament status breakdown + quick actions ---- *@
<div class="row g-3 mb-4">
<div class="col-md-7">
<div class="card shadow-sm h-100">
<div class="card-header" style="background-color:#003366; color:white;">
<h5 class="mb-0">School Standings</h5>
<h5 class="mb-0"><i class="bi bi-trophy me-1"></i>Tournaments by Status</h5>
</div>
<div class="card-body">
<div class="d-flex flex-wrap gap-3">
<div class="text-center">
<div class="h3 mb-0">@Model.DraftTournaments</div>
<span class="badge bg-secondary">Draft</span>
</div>
<div class="text-center">
<div class="h3 mb-0">@Model.RegistrationTournaments</div>
<span class="badge bg-info">Registration</span>
</div>
<div class="text-center">
<div class="h3 mb-0">@Model.ActiveTournaments</div>
<span class="badge bg-primary">In Progress</span>
</div>
<div class="text-center">
<div class="h3 mb-0">@Model.CompletedTournaments</div>
<span class="badge bg-success">Completed</span>
</div>
<div class="text-center">
<div class="h3 mb-0">@Model.ArchivedTournaments</div>
<span class="badge bg-dark">Archived</span>
</div>
</div>
<hr />
<a asp-controller="Tournament" asp-action="Index" class="btn btn-sm btn-outline-primary">
<i class="bi bi-list-ul me-1"></i>Manage tournaments
</a>
</div>
</div>
</div>
<div class="col-md-5">
<div class="card shadow-sm h-100">
<div class="card-header bg-dark text-white">
<h5 class="mb-0"><i class="bi bi-lightning-charge me-1"></i>Quick Actions</h5>
</div>
<div class="card-body d-flex flex-wrap gap-2 align-content-start">
<a asp-controller="Tournament" asp-action="Create" class="btn btn-outline-primary btn-sm"><i class="bi bi-plus-circle me-1"></i>New Tournament</a>
<a asp-controller="School" asp-action="Index" class="btn btn-outline-secondary btn-sm"><i class="bi bi-building me-1"></i>Schools</a>
<a asp-controller="Student" asp-action="Index" class="btn btn-outline-secondary btn-sm"><i class="bi bi-people me-1"></i>Students</a>
<a asp-controller="Report" asp-action="Index" class="btn btn-outline-success btn-sm"><i class="bi bi-file-earmark-bar-graph me-1"></i>Reports</a>
@if (User.IsInRole("Admin"))
{
<a asp-controller="ScoringConfig" asp-action="Index" class="btn btn-outline-dark btn-sm"><i class="bi bi-sliders me-1"></i>Scoring Config</a>
}
<a asp-controller="Help" asp-action="Index" class="btn btn-outline-info btn-sm"><i class="bi bi-question-circle me-1"></i>Help</a>
</div>
</div>
</div>
</div>
@if (selectedTournamentId.HasValue)
{
@* ---- Selected-tournament detail ---- *@
<h4 class="mb-3"><i class="bi bi-bullseye me-1"></i>@(selectedTournament?.Name ?? "Selected Tournament")</h4>
<div class="row g-3 mb-4">
<div class="col-6 col-md-4">
<div class="card bg-warning text-dark shadow-sm h-100">
<div class="card-body">
<h6 class="card-title"><i class="bi bi-card-checklist me-1"></i>Registrations</h6>
<h2 class="mb-0">@Model.TotalRegistrations</h2>
</div>
</div>
</div>
<div class="col-6 col-md-4">
<div class="card bg-success text-white shadow-sm h-100">
<div class="card-body">
<h6 class="card-title"><i class="bi bi-check2-circle me-1"></i>Events Scored</h6>
<h2 class="mb-0">@Model.EventsCompleted</h2>
<small>Results calculated</small>
</div>
</div>
</div>
<div class="col-6 col-md-4">
<div class="card bg-secondary text-white shadow-sm h-100">
<div class="card-body">
<h6 class="card-title"><i class="bi bi-hourglass-split me-1"></i>Events Pending</h6>
<h2 class="mb-0">@Model.EventsInProgress</h2>
<small>Awaiting results</small>
</div>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-lg-7">
<div class="card shadow-sm h-100">
<div class="card-header" style="background-color:#003366; color:white;">
<h5 class="mb-0"><i class="bi bi-bar-chart-line me-1"></i>School Standings</h5>
</div>
<div class="card-body p-0">
@if (Model.TopSchools.Any())
{
<table class="table table-striped table-hover mb-0">
<thead class="table-dark">
<tr>
@@ -91,6 +192,61 @@
}
</tbody>
</table>
}
else
{
<p class="text-muted p-3 mb-0">No points scored yet for this tournament.</p>
}
</div>
</div>
</div>
<div class="col-lg-5">
<div class="card shadow-sm h-100">
<div class="card-header bg-dark text-white">
<h5 class="mb-0"><i class="bi bi-activity me-1"></i>Recent Activity</h5>
</div>
<div class="card-body p-0">
@if (Model.RecentScores.Any())
{
<ul class="list-group list-group-flush">
@foreach (var s in Model.RecentScores)
{
<li class="list-group-item d-flex justify-content-between align-items-start">
<div>
<div class="fw-semibold">@s.StudentName</div>
<small class="text-muted">@s.EventName — @s.EventLevelName · @s.RawPerformance.ToString("0.00")</small>
</div>
<div class="text-end text-nowrap">
@if (s.Placement != null)
{
<span class="badge bg-warning text-dark">#@s.Placement</span>
}
<div><small class="text-muted">@s.RecordedAt.ToString("MMM d, HH:mm")</small></div>
</div>
</li>
}
</ul>
}
else
{
<p class="text-muted p-3 mb-0">No scores recorded yet for this tournament.</p>
}
</div>
</div>
</div>
</div>
}
else if (tournamentList.Count > 1)
{
<div class="alert alert-info">
<i class="bi bi-info-circle me-1"></i>
You have multiple active tournaments. <strong>Select one above</strong> to see its registrations, school standings, and recent scoring activity.
</div>
}
else
{
<div class="alert alert-secondary">
<i class="bi bi-info-circle me-1"></i>
No active tournaments yet. <a asp-controller="Tournament" asp-action="Create">Create a tournament</a> to get started.
</div>
}

View File

@@ -0,0 +1,409 @@
@{
ViewData["Title"] = "Help & User Guide";
Layout = "_HelpLayout";
}
<div class="help-hero">
<h1 class="mb-1"><i class="bi bi-life-preserver me-2"></i>Help &amp; User Guide</h1>
<p class="mb-0 lead">Everything you need to run a track &amp; field competition — tournaments, scoring, results, and reports. Use the search box to jump straight to an answer.</p>
</div>
<div class="help-search-bar">
<div class="position-relative">
<i class="bi bi-search help-search-icon"></i>
<input id="helpSearch" type="search" class="form-control" autocomplete="off"
placeholder="Search the help — e.g. 'archive', 'tie', 'scoring constants', 'advancement'…" />
</div>
<div id="helpCount" class="small text-muted mt-1"></div>
</div>
<div id="helpNoResults" class="alert alert-warning help-noresults">
<i class="bi bi-emoji-frown me-1"></i> No help topics matched your search. Try a different word.
</div>
<div class="help-layout">
<nav class="help-toc" id="helpToc" aria-label="Help contents">
<div class="fw-semibold text-uppercase small text-muted mb-2">Contents</div>
<a href="#getting-started">Getting started</a>
<a href="#tournaments">Tournaments &amp; statuses</a>
<a href="#archiving">Archiving tournaments</a>
<a href="#events">Events</a>
<a href="#track">Track events</a>
<a href="#field">Field events</a>
<a href="#highjump">High jump</a>
<a href="#wa-points">World Athletics points</a>
<a href="#placement-points">Placement points</a>
<a href="#schools-students">Schools &amp; students</a>
<a href="#reports">Reports &amp; Excel</a>
<a href="#pagination">Lists &amp; pagination</a>
<a href="#dashboard">Dashboard</a>
<a href="#faq">FAQ / troubleshooting</a>
</nav>
<div class="help-content">
@* ============ GETTING STARTED ============ *@
<section class="help-section" id="getting-started">
<h2><i class="bi bi-compass me-1"></i>Getting started</h2>
<div class="help-entry">
<h3><i class="bi bi-signpost-2 me-1"></i>What this system does</h3>
<p>The Dominica Sports Division system manages school track &amp; field competitions end to end: it holds your <strong>schools</strong> and <strong>students</strong>, lets you build <strong>tournaments</strong> made up of <strong>events</strong> at different age/sex <strong>levels</strong>, captures results for track, field and high&nbsp;jump, converts them to points, and produces <strong>standings and reports</strong>.</p>
</div>
<div class="help-entry">
<h3><i class="bi bi-diagram-3 me-1"></i>The big picture — how a competition flows</h3>
<ol>
<li>Add your <strong>Schools</strong> and their <strong>Students</strong>.</li>
<li>Create a <strong>Tournament</strong> and add the <strong>events</strong> you'll run (each at one or more age/sex levels).</li>
<li>Open registration and <strong>register students</strong> into events.</li>
<li>Run each event and record results in <strong>Track Events</strong>, <strong>Field Events</strong> or <strong>High Jump</strong>.</li>
<li>Calculate points/placements; view <strong>school standings</strong> on the Dashboard and run <strong>Reports</strong>.</li>
</ol>
</div>
<div class="help-entry">
<h3><i class="bi bi-search me-1"></i>Using this help page</h3>
<p>Type any word into the search box at the top — topics that don't match are hidden as you type. Clear the box to see everything again. On wide screens the left-hand contents menu jumps you to a section.</p>
</div>
</section>
@* ============ TOURNAMENTS ============ *@
<section class="help-section" id="tournaments">
<h2><i class="bi bi-trophy me-1"></i>Tournaments &amp; statuses</h2>
<div class="help-entry">
<h3><i class="bi bi-arrow-right-circle me-1"></i>The tournament lifecycle</h3>
<p>A tournament moves through four statuses, in order:</p>
<ul>
<li><span class="badge bg-secondary">Draft</span> — you're still setting it up (adding events).</li>
<li><span class="badge bg-info">Registration</span> — students can be registered into events.</li>
<li><span class="badge bg-primary">In Progress</span> — the competition is running and results are being recorded.</li>
<li><span class="badge bg-success">Completed</span> — all events are finished and scored.</li>
</ul>
<p>Open a tournament's <strong>Details</strong> page and use the buttons in the top-right (<em>Open Registration → Start Tournament → Complete</em>) to move it forward one step at a time.</p>
</div>
<div class="help-entry">
<h3><i class="bi bi-arrow-counterclockwise me-1"></i>Reverting a status set by mistake</h3>
<p>Every forward step can be undone. On the Details page you'll also see backward buttons: <strong>Back to Draft</strong>, <strong>Back to Registration</strong>, and — if a tournament was marked Completed by mistake — <strong>Reopen Tournament</strong> (which returns it to In Progress). Nothing is lost; the status simply changes back.</p>
</div>
<div class="help-entry">
<h3><i class="bi bi-check2-square me-1"></i>Why "Complete" asks for confirmation</h3>
<p>Marking a tournament <strong>Completed</strong> pops up a confirmation because it signals the competition is over. If you click it by accident, just confirm you didn't mean to (or use <strong>Reopen Tournament</strong> afterwards to set it back to In Progress).</p>
</div>
</section>
@* ============ ARCHIVING ============ *@
<section class="help-section" id="archiving">
<h2><i class="bi bi-archive me-1"></i>Archiving tournaments</h2>
<div class="help-entry">
<h3><i class="bi bi-box-seam me-1"></i>What archiving does</h3>
<p>Archiving <strong>hides</strong> an old tournament from the default list to reduce clutter — it is <strong>not</strong> deleted. All its events, registrations and results are kept and can be restored at any time with <strong>Unarchive</strong>.</p>
</div>
<div class="help-entry">
<h3><i class="bi bi-shield-check me-1"></i>Only completed tournaments can be archived</h3>
<p>The <strong>Archive</strong> button only appears once a tournament's status is <span class="badge bg-success">Completed</span>. This prevents archiving something that's still in draft, registration, or mid-competition. If you need to archive an active one, finish it (or it isn't ready to archive yet).</p>
</div>
<div class="help-entry">
<h3><i class="bi bi-eye me-1"></i>Where do archived tournaments go? / "I can't see my tournament"</h3>
<p>Archived tournaments are hidden on the Tournaments page by default. To see them, tick the <strong>Show archived</strong> switch near the top of the list — they reappear with an <span class="badge bg-dark">Archived</span> badge. A small counter on the switch tells you how many are currently hidden. Untick it to hide them again.</p>
</div>
<div class="help-entry">
<h3><i class="bi bi-arrow-clockwise me-1"></i>Restoring an archived tournament</h3>
<p>Turn on <strong>Show archived</strong>, find the tournament, and click <strong>Unarchive</strong> (on the list row or its Details page). It returns to the normal list immediately, keeping whatever status it had.</p>
</div>
</section>
@* ============ EVENTS ============ *@
<section class="help-section" id="events">
<h2><i class="bi bi-calendar-event me-1"></i>Events</h2>
<div class="help-entry">
<h3><i class="bi bi-tags me-1"></i>The three event categories</h3>
<ul>
<li><strong>Track</strong> — races timed in seconds (lower is better): 80 m, 100 m, 200 m, … 5000 m, hurdles, relays.</li>
<li><strong>Field</strong> — distance/height events measured in metres (higher is better): Long Jump, Triple Jump, Shot Put, Discus, Javelin, Cricket Ball.</li>
<li><strong>High Jump</strong> — its own category because it uses a rising bar with multiple attempts, scored differently from other field events.</li>
</ul>
<p>The <strong>Events</strong> page lists every event grouped by category. Each event can be offered at several <strong>levels</strong> (age/sex divisions) within a tournament.</p>
</div>
<div class="help-entry">
<h3><i class="bi bi-plus-square me-1"></i>Adding events to a tournament</h3>
<p>On a tournament's <strong>Details</strong> page, use <strong>Add Event Level</strong> to pick an event and a level (e.g. "100 m" for "Under 16 Boys"). Each event+level combination is where registrations and scoring happen.</p>
</div>
</section>
@* ============ TRACK ============ *@
<section class="help-section" id="track">
<h2><i class="bi bi-stopwatch me-1"></i>Track events</h2>
<div class="help-entry">
<h3><i class="bi bi-list-ol me-1"></i>How a track event flows</h3>
<ol>
<li><strong>Create rounds</strong> — a preliminary <em>Heat</em> round and a <em>Final</em> (add a Semi-Final too for big fields).</li>
<li><strong>Seed heats</strong> — athletes are split into heats of at most <strong>8</strong> lanes.</li>
<li><strong>Record times</strong> for each lane, per heat tab.</li>
<li><strong>Calculate Advancement</strong> — marks who progresses (see "Top N + M FL" below).</li>
<li><strong>Send Advancers to Next Round</strong> — copies the qualifiers into the next round.</li>
<li>Record the final's times, then <strong>Calculate Final Standings</strong> to award places &amp; points.</li>
</ol>
</div>
<div class="help-entry">
<h3><i class="bi bi-input-cursor me-1"></i>What does the "3, 3, 2" dropdown (Add Round) mean?</h3>
<p>When you add a round you set its <strong>advancement rule</strong>: <strong>Advance Top N</strong> per heat + <strong>M Fastest Losers</strong>. For example "Top&nbsp;3 + 2 FL" means the first 3 in each heat qualify automatically, plus the 2 fastest remaining athletes across all heats. For a <strong>Final</strong> round these are not used (a final awards points instead of advancing anyone), so the advance fields are hidden.</p>
</div>
<div class="help-entry">
<h3><i class="bi bi-question-circle me-1"></i>What does "Top&nbsp;0 + 0 FL" mean, and does it update?</h3>
<p>That's just a round whose advancement rule is still set to zero — nobody is set to advance yet. It updates the moment you give the round real advancement numbers (e.g. Top&nbsp;3 + 2&nbsp;FL) when you create or edit it. On a <strong>Final</strong> round it stays at zero on purpose, because a final doesn't advance anyone — it's the last round.</p>
</div>
<div class="help-entry">
<h3><i class="bi bi-trophy me-1"></i>Heats never exceed 8 — and points come from the final</h3>
<p>Seeding always caps a heat at <strong>8 lanes</strong>; with more athletes the system makes additional heats. <strong>Points and placements are awarded only in the final round</strong> via <strong>Calculate Final Standings</strong> — the finishers are ranked fastest-first and given placement points (10, 8, 6, 5…) and World Athletics points for their time.</p>
</div>
<div class="help-entry">
<h3><i class="bi bi-arrow-repeat me-1"></i>Re-running the calculation</h3>
<p>If you correct a time after calculating, just click <strong>Calculate Final Standings</strong> again — it recomputes places and points from the current times. The "Back" button on a heat page returns you to that specific event's page.</p>
</div>
</section>
@* ============ FIELD ============ *@
<section class="help-section" id="field">
<h2><i class="bi bi-rulers me-1"></i>Field events</h2>
<div class="help-entry">
<h3><i class="bi bi-pencil-square me-1"></i>Recording and scoring a field event</h3>
<ol>
<li>Enter each athlete's best <strong>mark in metres</strong> and Save.</li>
<li>Click <strong>Calculate Points</strong> — converts each mark to World Athletics points.</li>
<li>Click <strong>Calculate Placements</strong> — ranks athletes by points and awards placement points (10, 8, 6…).</li>
</ol>
<p>Field events are measured in <strong>metres</strong> (e.g. a 6.42 m long jump), where a bigger number is better.</p>
</div>
</section>
@* ============ HIGH JUMP ============ *@
<section class="help-section" id="highjump">
<h2><i class="bi bi-arrow-up-circle me-1"></i>High jump</h2>
<div class="help-entry">
<h3><i class="bi bi-bar-chart-steps me-1"></i>Recording a high jump competition</h3>
<ol>
<li><strong>Add the bar heights</strong> (e.g. 1.40, 1.45, 1.50 …) in ascending order.</li>
<li>For each athlete at each bar, record attempts: <span class="text-success fw-bold">O</span> = cleared, <span class="text-danger fw-bold">X</span> = failed, <span class="text-muted fw-bold"></span> = passed. Three failures in a row eliminates an athlete.</li>
<li>Click <strong>Calculate Results</strong> to rank everyone and award points.</li>
</ol>
</div>
<div class="help-entry">
<h3><i class="bi bi-table me-1"></i>Where are the points? Do I need to click a button?</h3>
<p>The grid of O/X/ only records attempts. The <strong>Results</strong> table (placement, best height, points) appears <strong>after you click "Calculate Results"</strong>. That button takes everyone's clearances, ranks them, and writes their placement and points. Until then the Results section shows a prompt. It's safe to re-run any time after fixing an attempt.</p>
</div>
<div class="help-entry">
<h3><i class="bi bi-shuffle me-1"></i>How ties are broken (countback)</h3>
<p>Athletes are ranked by World Athletics high-jump countback:</p>
<ol>
<li><strong>Highest bar cleared</strong> wins.</li>
<li>If tied, <strong>fewest misses at that top bar</strong> wins.</li>
<li>If still tied, <strong>fewest total misses</strong> across the whole competition wins.</li>
</ol>
<p>So yes — earlier bars are consulted, but only as the second tie-breaker, after comparing misses at the final height.</p>
</div>
<div class="help-entry">
<h3><i class="bi bi-people me-1"></i>Genuine ties &amp; jump-offs</h3>
<p>If two athletes are identical on all three countback measures they are a true tie: they <strong>share the same placement</strong>, and the points for the positions they occupy are <strong>pooled and split equally</strong> (e.g. a tie for 1st &amp; 2nd gives each (10+8)÷2 = 9). A tie for <strong>1st place</strong> can't be settled by countback, so the system shows a <strong>jump-off required</strong> warning naming the athletes; arrange a jump-off and adjust as needed.</p>
</div>
</section>
@* ============ WA POINTS ============ *@
<section class="help-section" id="wa-points">
<h2><i class="bi bi-calculator me-1"></i>World Athletics points (Scoring Constants)</h2>
<div class="help-entry">
<h3><i class="bi bi-question-octagon me-1"></i>What are the Scoring Constants for?</h3>
<p>They convert any performance — a time, a distance, a height — onto a single common <strong>points scale</strong>, so results in completely different events can be compared and added together fairly (the same idea behind the decathlon). Each event has its own three constants — <strong>A, B and C</strong> — editable on the <strong>Scoring Config</strong> page.</p>
</div>
<div class="help-entry">
<h3><i class="bi bi-function me-1"></i>The two formulas</h3>
<p>The shape depends on whether lower or higher is better:</p>
<p><span class="help-formula">Track (time): Points = A × (B T)^C</span> &nbsp;— T is the time in seconds.</p>
<p><span class="help-formula">Field &amp; High Jump: Points = A × (P B)^C</span> &nbsp;— P is the mark in metres.</p>
<p>The result is rounded down to a whole number, and a performance worse than B scores <strong>0</strong>.</p>
</div>
<div class="help-entry">
<h3><i class="bi bi-sliders me-1"></i>What A, B and C each mean</h3>
<ul>
<li><strong>B — baseline/cutoff:</strong> the threshold worth ~0 points. Track: the slowest time that still scores. Field/Jump: the smallest mark that still scores.</li>
<li><strong>A — scale factor:</strong> sets the overall magnitude — how many points each unit of margin above the baseline is worth.</li>
<li><strong>C — curve shape:</strong> the exponent. C&nbsp;&gt;&nbsp;1 means better performances earn disproportionately more points (rewards excellence).</li>
</ul>
</div>
<div class="help-entry">
<h3><i class="bi bi-123 me-1"></i>Worked example — High Jump (A = 585.63, B = 0.75, C = 1.42)</h3>
<p>Using <code>Points = 585.63 × (Height 0.75)^1.42</code>:</p>
<ul>
<li>1.60 m → 585.63 × (0.85)^1.42 = <strong>464</strong></li>
<li>1.55 m → 585.63 × (0.80)^1.42 = <strong>426</strong></li>
<li>1.50 m → 585.63 × (0.75)^1.42 = <strong>389</strong></li>
</ul>
<p>B = 0.75 means a "jump" of 0.75 m or lower scores 0. For comparison, 100 m uses <code>26 × (18 T)^1.81</code>, so a 100 m slower than 18 s scores 0.</p>
</div>
<div class="help-entry">
<h3><i class="bi bi-exclamation-triangle me-1"></i>Why does an event show 0 points?</h3>
<p>If an event's <strong>Points</strong> column is 0 for everyone, it usually has <strong>no scoring constants configured</strong>. Add A/B/C for it on the <strong>Scoring Config</strong> page and recalculate. (Placement points still work even without constants.)</p>
</div>
</section>
@* ============ PLACEMENT POINTS ============ *@
<section class="help-section" id="placement-points">
<h2><i class="bi bi-award me-1"></i>Placement points</h2>
<div class="help-entry">
<h3><i class="bi bi-distribute-vertical me-1"></i>Placement points vs World Athletics points</h3>
<p>These are two different columns that reward different things:</p>
<ul>
<li><strong>Points (World Athletics):</strong> rewards the <em>quality of the performance</em> itself — a great mark scores high regardless of who else competed.</li>
<li><strong>Placement points:</strong> rewards <em>finishing position</em> — by default 10 / 8 / 6 / 5 / 4 / 3 / 2 / 1 for 1st8th — and these are what feed the <strong>school standings</strong>.</li>
</ul>
<p>An athlete earns both. You can change the placement values on the <strong>Scoring Config</strong> page.</p>
</div>
</section>
@* ============ SCHOOLS & STUDENTS ============ *@
<section class="help-section" id="schools-students">
<h2><i class="bi bi-people me-1"></i>Schools &amp; students</h2>
<div class="help-entry">
<h3><i class="bi bi-building me-1"></i>Managing schools</h3>
<p>The <strong>Schools</strong> page lists every institution with its zone, level and student count. Add or edit schools here; each student belongs to one school, and school standings are tallied from their students' placement points.</p>
</div>
<div class="help-entry">
<h3><i class="bi bi-person-plus me-1"></i>Managing students &amp; eligibility</h3>
<p>Add students under <strong>Students</strong>, each with a date of birth and sex. Age/sex determine which event <strong>levels</strong> they're eligible for. A tournament event level can <strong>waive the age restriction</strong> if needed (toggled on the tournament's Details page).</p>
</div>
</section>
@* ============ REPORTS ============ *@
<section class="help-section" id="reports">
<h2><i class="bi bi-file-earmark-bar-graph me-1"></i>Reports &amp; Excel export</h2>
<div class="help-entry">
<h3><i class="bi bi-table me-1"></i>Available reports</h3>
<p>The <strong>Reports</strong> section includes Popular Events, Registration by Gender, Event/School breakdowns, Students by School, Scores by Event, and Student Points. Most can be filtered by tournament, zone, school or event.</p>
</div>
<div class="help-entry">
<h3><i class="bi bi-file-earmark-excel me-1"></i>Exporting to Excel</h3>
<p>Every report has an <strong>Export to Excel</strong> button that downloads the current report as an <code>.xlsx</code> spreadsheet for printing or sharing.</p>
</div>
</section>
@* ============ PAGINATION ============ *@
<section class="help-section" id="pagination">
<h2><i class="bi bi-card-list me-1"></i>Lists &amp; pagination</h2>
<div class="help-entry">
<h3><i class="bi bi-arrow-left-right me-1"></i>Page size &amp; navigation</h3>
<p>Long lists (students, schools, tournaments, etc.) are paginated. Use the <strong>Show … per page</strong> dropdown to display 20, 50, 100, 500 or <strong>All</strong> items, and the page numbers to move between pages. Your filters are kept as you page.</p>
</div>
</section>
@* ============ DASHBOARD ============ *@
<section class="help-section" id="dashboard">
<h2><i class="bi bi-speedometer2 me-1"></i>Dashboard</h2>
<div class="help-entry">
<h3><i class="bi bi-grid-1x2 me-1"></i>What the Dashboard shows</h3>
<p>The Dashboard summarises the system at a glance: totals for students, schools, events and recorded scores; a breakdown of tournaments by status; and <strong>Quick Actions</strong> shortcuts. Pick a tournament from the selector to see its registrations, how many events are scored vs pending, the live <strong>school standings</strong>, and a <strong>recent activity</strong> feed of the latest results.</p>
</div>
</section>
@* ============ FAQ ============ *@
<section class="help-section" id="faq">
<h2><i class="bi bi-patch-question me-1"></i>FAQ / troubleshooting</h2>
<div class="help-entry">
<h3><i class="bi bi-dash-circle me-1"></i>An event's points are all zero</h3>
<p>The event likely has no <strong>scoring constants</strong>. Configure A/B/C on the Scoring Config page, then recalculate. Placement points are unaffected.</p>
</div>
<div class="help-entry">
<h3><i class="bi bi-eye-slash me-1"></i>A tournament disappeared from the list</h3>
<p>It was probably <strong>archived</strong>. Tick <strong>Show archived</strong> on the Tournaments page to reveal it, then <strong>Unarchive</strong> if you want it back in the normal list.</p>
</div>
<div class="help-entry">
<h3><i class="bi bi-x-octagon me-1"></i>High jump shows no results</h3>
<p>Click <strong>Calculate Results</strong> on the High Jump page — the Results table only fills in after you run it.</p>
</div>
<div class="help-entry">
<h3><i class="bi bi-trophy me-1"></i>Track event has no points</h3>
<p>Track points are awarded only in the <strong>final</strong> round. Make sure you've created a Final round, recorded its times, and clicked <strong>Calculate Final Standings</strong>.</p>
</div>
<div class="help-entry">
<h3><i class="bi bi-lock me-1"></i>I can't archive a tournament</h3>
<p>Only <span class="badge bg-success">Completed</span> tournaments can be archived — the button appears once a tournament is marked Completed.</p>
</div>
</section>
</div>
</div>
@section Scripts {
<script>
(function () {
var search = document.getElementById('helpSearch');
var entries = Array.prototype.slice.call(document.querySelectorAll('.help-entry'));
var sections = Array.prototype.slice.call(document.querySelectorAll('.help-section'));
var noResults = document.getElementById('helpNoResults');
var toc = document.getElementById('helpToc');
var countEl = document.getElementById('helpCount');
// Precompute searchable text for each entry (heading + body).
entries.forEach(function (e) { e._text = (e.textContent || '').toLowerCase(); });
function applyFilter() {
var q = search.value.trim().toLowerCase();
var visible = 0;
entries.forEach(function (e) {
var match = q === '' || e._text.indexOf(q) !== -1;
e.style.display = match ? '' : 'none';
if (match) visible++;
});
// Hide a section header when none of its entries match.
sections.forEach(function (s) {
var es = s.querySelectorAll('.help-entry');
var anyVisible = false;
for (var i = 0; i < es.length; i++) { if (es[i].style.display !== 'none') { anyVisible = true; break; } }
s.style.display = (!anyVisible && q !== '') ? 'none' : '';
});
noResults.style.display = (visible === 0 && q !== '') ? 'block' : 'none';
if (toc) toc.style.visibility = q !== '' ? 'hidden' : 'visible';
countEl.textContent = q !== '' ? (visible + ' topic' + (visible === 1 ? '' : 's') + ' found') : '';
}
search.addEventListener('input', applyFilter);
// Pressing Escape clears the search.
search.addEventListener('keydown', function (e) { if (e.key === 'Escape') { search.value = ''; applyFilter(); } });
})();
</script>
}

View File

@@ -0,0 +1,106 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Sports Division Help</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
<style>
:root {
--help-primary: #003366;
--help-accent: #FFD700;
}
body {
background-color: #f4f6f9;
color: #212529;
}
.help-topbar {
background-color: var(--help-primary);
color: #fff;
padding: .6rem 1rem;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 1030;
box-shadow: 0 2px 6px rgba(0,0,0,.15);
}
.help-topbar .brand { font-weight: 600; font-size: 1.1rem; }
.help-topbar .brand i { color: var(--help-accent); }
.help-topbar a.back-link { color: #fff; text-decoration: none; }
.help-topbar a.back-link:hover { color: var(--help-accent); }
.help-wrap { max-width: 1100px; margin: 0 auto; padding: 1.5rem 1rem 4rem; }
.help-hero {
background: linear-gradient(135deg, var(--help-primary), #004080);
color: #fff;
border-radius: .75rem;
padding: 2rem;
margin-bottom: 1.25rem;
}
.help-search-bar {
position: sticky;
top: 56px;
z-index: 1020;
background: #f4f6f9;
padding: .75rem 0;
margin-bottom: 1rem;
}
.help-search-bar input {
font-size: 1.05rem;
padding: .65rem 1rem .65rem 2.6rem;
}
.help-search-icon {
position: absolute; left: .9rem; top: 50%; transform: translateY(-50%);
color: #6c757d; pointer-events: none;
}
.help-layout { display: grid; grid-template-columns: 240px 1fr; gap: 1.5rem; }
@@media (max-width: 800px) { .help-layout { grid-template-columns: 1fr; } .help-toc { display: none; } }
.help-toc {
position: sticky; top: 130px; align-self: start;
max-height: calc(100vh - 150px); overflow-y: auto;
}
.help-toc a {
display: block; padding: .3rem .5rem; color: #495057; text-decoration: none;
border-left: 3px solid transparent; font-size: .92rem; border-radius: 0 .25rem .25rem 0;
}
.help-toc a:hover { background: #e9ecef; border-left-color: var(--help-accent); color: var(--help-primary); }
.help-section { scroll-margin-top: 130px; margin-bottom: 2rem; }
.help-section > h2 {
color: var(--help-primary); border-bottom: 2px solid var(--help-accent);
padding-bottom: .35rem; margin-bottom: 1rem; font-size: 1.45rem;
}
.help-entry {
background: #fff; border: 1px solid #e3e6ea; border-radius: .5rem;
padding: 1rem 1.25rem; margin-bottom: .85rem; box-shadow: 0 1px 2px rgba(0,0,0,.04);
}
.help-entry h3 { font-size: 1.08rem; margin-bottom: .5rem; color: #003366; }
.help-entry h3 i { color: var(--help-accent); }
.help-entry p:last-child, .help-entry ul:last-child, .help-entry ol:last-child { margin-bottom: 0; }
.help-entry code, .help-formula {
background: #eef2f7; border-radius: .25rem; padding: .1rem .35rem; font-size: .92em;
}
.help-formula { display: inline-block; padding: .4rem .7rem; margin: .25rem 0; font-weight: 600; }
mark { background: var(--help-accent); padding: 0 .1rem; }
.help-noresults { display: none; }
</style>
</head>
<body>
<div class="help-topbar">
<span class="brand"><i class="bi bi-life-preserver"></i> Sports Division Help</span>
<a class="back-link" asp-controller="Dashboard" asp-action="Index"><i class="bi bi-arrow-left-circle me-1"></i>Back to App</a>
</div>
<div class="help-wrap">
@RenderBody()
</div>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

View File

@@ -29,6 +29,12 @@
<span class="d-none d-sm-inline">@displayName</span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item" asp-controller="Help" asp-action="Index">
<i class="bi bi-question-circle me-2"></i>Help &amp; User Guide
</a>
</li>
<li><hr class="dropdown-divider" /></li>
<li>
<form asp-controller="Account" asp-action="Logout" method="post">
@Html.AntiForgeryToken()