Files
chapter-organizer/WebApp/Components/Pages/Home.razor
T
poprhythm 675f04afec Add CalendarService and integrate calendar functionality into components
This commit introduces the CalendarService, which provides methods for retrieving all calendar items and upcoming events. The service is integrated into various components, including the Calendar and Home pages, enhancing the calendar functionality by allowing users to view upcoming events and meeting histories. Additionally, the Index.razor component is updated to parse query parameters for date selection, improving user experience. The layout of the MeetingHistoryDetailDialog and SaveMeetingHistoryDialog components is also refined for better organization and responsiveness. These changes collectively enhance the calendar feature and improve the overall user interface in managing events and meetings.
2026-01-27 22:38:52 -05:00

451 lines
16 KiB
Plaintext

@page "/"
@attribute [Authorize]
@using Microsoft.EntityFrameworkCore
@using WebApp.Models
@using Core.Entities
@using WebApp.Services
@using WebApp.Components.Shared.Components
@using Heron.MudCalendar
@inject IConfiguration Configuration
@inject AppDbContext Context
@inject INotesService NotesService
@inject IDialogService DialogService
@inject ITeamMeetingHistoryService TeamMeetingHistoryService
@inject ICalendarService CalendarService
@inject NavigationManager Navigation
@implements IAsyncDisposable
<PageTitle>@Configuration["ChapterSettings:Name"] - TSA Chapter Organizer</PageTitle>
<MudPaper Elevation="0" Class="mb-6">
<div class="d-flex flex-column align-center text-center mb-4">
<MudImage Fluid="true" Src="TCO_Title.png" Alt="TSA Chapter Organizer" Class="mb-4" Style="width: 100%; max-width: 600px; height: auto;" />
<MudHidden Breakpoint="Breakpoint.SmAndDown">
<MudText Typo="Typo.h3" Class="mb-2">
<strong>@Configuration["ChapterSettings:Name"]</strong>
</MudText>
</MudHidden>
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudText Typo="Typo.h5" Class="mb-2">
<strong>@Configuration["ChapterSettings:Name"]</strong>
</MudText>
</MudHidden>
<MudText Typo="Typo.h6" Color="Color.Info">
@Configuration["ChapterSettings:CompetitionYear"] Competition Year
</MudText>
</div>
</MudPaper>
@if (_pinnedNotes.Any())
{
<MudPaper Elevation="0" Class="mb-4">
<MudGrid>
@foreach (var note in _pinnedNotes)
{
<NoteCard Note="@note" OnNoteChanged="HandleNoteChanged" />
}
</MudGrid>
</MudPaper>
}
@if (!_hasStudents)
{
<!-- Getting Started: No students yet -->
<MudPaper Elevation="0" Class="mb-4">
<MudText Typo="Typo.h4" Color="Color.Primary">Getting Started</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">Add your chapter's students to begin</MudText>
</MudPaper>
<MudGrid>
<DashboardCard Icon="@AppIcons.Student"
Title="Students"
Count="@_studentCount"
Subtitle="Add Students"
NavigateUrl="/students"
Emphasized="true">
<MudText Typo="Typo.caption" Class="mt-2">Import or add your chapter roster</MudText>
</DashboardCard>
<DashboardCard Icon="@AppIcons.Events"
Title="Events"
Count="@_eventCount"
Subtitle="Total Events"
Caption="@($"{_teamEventsCount} Team | {_individualEventsCount} Individual")"
NavigateUrl="/events" />
</MudGrid>
}
else if (!_hasTeams)
{
<!-- Team Building: Students exist but no teams yet -->
<MudPaper Elevation="0" Class="mb-4">
<MudText Typo="Typo.h4" Color="Color.Primary">Team Building</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">Collect event rankings and build your teams</MudText>
</MudPaper>
<MudGrid>
<DashboardCard Icon="@AppIcons.EventRank"
Title="Event Ranking"
Caption="Collect student event preferences"
NavigateUrl="/students/event-ranking"
Emphasized="true"/>
<DashboardCard Icon="@AppIcons.TeamAssignment"
Title="Team Assignment"
Caption="Build optimal teams"
NavigateUrl="/teams/assignment"
Emphasized="true"/>
</MudGrid>
<MudPaper Elevation="0" Class="my-4">
<MudText Typo="Typo.h4">Chapter Data</MudText>
</MudPaper>
<MudGrid>
<DashboardCard Icon="@AppIcons.Student"
Title="Students"
Count="@_studentCount"
Subtitle="Active Students"
NavigateUrl="/students">
@if (!string.IsNullOrEmpty(_gradeDistribution) && _gradeDistribution != "No students yet")
{
<MudStack>
<MudText Typo="Typo.caption">@((MarkupString)_gradeDistribution)</MudText>
</MudStack>
}
</DashboardCard>
<DashboardCard Icon="@AppIcons.Events"
Title="Events"
Count="@_eventCount"
Subtitle="Total Events"
Caption="@($"{_teamEventsCount} Team | {_individualEventsCount} Individual")"
NavigateUrl="/events" />
</MudGrid>
}
else
{
<!-- Normal view: Students and teams exist -->
<MudPaper Elevation="0" Class="mb-4">
<MudText Typo="Typo.h4">Scheduling</MudText>
</MudPaper>
<MudGrid>
<DashboardCard Icon="@AppIcons.Scheduler"
Title="Meeting Schedule Planner"
NavigateUrl="/meeting-schedule">
@if (_recentMeetings.Any())
{
<MudGrid Spacing="2">
<MudItem xs="12" sm="12" md="7">
<MudText Typo="Typo.subtitle2" Class="mb-2" Style="font-weight: 600;">Recent Meetings</MudText>
<MudStack Spacing="1">
@foreach (var meeting in _recentMeetings)
{
<div @onclick="@(() => ShowMeetingDetails(meeting))"
@onclick:stopPropagation="true"
style="cursor: pointer; color: var(--mud-palette-primary);">
<MudText Typo="Typo.body2" Style="text-decoration: underline;">
@meeting.MeetingDate.ToString("MMM d, yyyy")
</MudText>
</div>
}
</MudStack>
</MudItem>
<MudItem xs="12" sm="12" md="5">
<MudStack Spacing="1" AlignItems="AlignItems.Start">
<div @onclick:stopPropagation="true">
<MudTooltip Text="View meeting history">
<MudButton StartIcon="@Icons.Material.Filled.History"
Variant="Variant.Outlined"
Color="Color.Default"
Href="/meeting-schedule/history">
View History
</MudButton>
</MudTooltip>
</div>
</MudStack>
</MudItem>
</MudGrid>
}
else
{
<MudText Typo="Typo.caption" Class="mt-2">Optimize meeting times</MudText>
}
</DashboardCard>
<DashboardCard Icon="@AppIcons.EventCalendar"
Title="Event Calendar"
NavigateUrl="/calendar">
@if (_nextCalendarItems.Any())
{
<MudText Typo="Typo.subtitle2" Class="mb-2" Style="font-weight: 600;">Next Events</MudText>
<MudStack Spacing="1">
@foreach (var item in _nextCalendarItems)
{
var itemDate = item.Start.Date;
var itemName = item.ItemType == CalendarItemType.Event && item.EventItem != null
? (item.EventItem.EventDefinition?.ShortName ?? item.EventItem.EventOccurrenceData?.Name ?? "Event")
: "Team Meeting";
var dateStr = itemDate.ToString("yyyy-MM-dd");
<div @onclick="@(() => NavigateToCalendar(dateStr))"
@onclick:stopPropagation="true"
style="cursor: pointer; color: var(--mud-palette-primary);">
<MudText Typo="Typo.body2" Style="text-decoration: underline;">
@itemName - @itemDate.ToString("MMM d, yyyy")
</MudText>
</div>
}
</MudStack>
}
else
{
<MudText Typo="Typo.caption" Class="mt-2">Conference schedules</MudText>
}
</DashboardCard>
</MudGrid>
<MudPaper Elevation="0" Class="my-4">
<MudText Typo="Typo.h4">Teams & Registration</MudText>
</MudPaper>
<MudGrid>
<DashboardCard Icon="@AppIcons.Teams"
Title="Teams"
Count="@_teamCount"
Subtitle="Total Teams"
Caption="@($"{_groupTeamsCount} Team | {_individualTeamsCount} Individual")"
NavigateUrl="/teams" />
<DashboardCard Icon="@AppIcons.Registration"
Title="Registration"
Caption="View student registrations"
NavigateUrl="/students/teams"/>
</MudGrid>
<MudPaper Elevation="0" Class="my-4">
<MudText Typo="Typo.h4">Chapter Data</MudText>
</MudPaper>
<MudGrid>
<DashboardCard Icon="@AppIcons.Student"
Title="Students"
Count="@_studentCount"
Subtitle="Active Students"
NavigateUrl="/students">
@if (!string.IsNullOrEmpty(_gradeDistribution) && _gradeDistribution != "No students yet")
{
<MudStack>
<MudText Typo="Typo.caption">@((MarkupString)_gradeDistribution)</MudText>
</MudStack>
}
</DashboardCard>
<DashboardCard Icon="@AppIcons.Events"
Title="Events"
Count="@_eventCount"
Subtitle="Total Events"
Caption="@($"{_teamEventsCount} Team | {_individualEventsCount} Individual")"
NavigateUrl="/events" />
</MudGrid>
<MudPaper Elevation="0" Class="my-4">
<MudText Typo="Typo.h4">Team Building</MudText>
</MudPaper>
<MudGrid>
<DashboardCard Icon="@AppIcons.EventRank"
Title="Event Ranking"
Caption="Student event preferences"
NavigateUrl="/students/event-ranking"/>
<DashboardCard Icon="@AppIcons.TeamAssignment"
Title="Team Assignment"
Caption="Build optimal teams"
NavigateUrl="/teams/assignment"/>
</MudGrid>
}
@code {
private int _eventCount;
private int _individualEventsCount;
private int _teamEventsCount;
private int _studentCount;
private string _gradeDistribution = "";
private int _teamCount;
private int _individualTeamsCount;
private int _groupTeamsCount;
private int _meetingHistoryCount;
private List<TeamMeetingHistory> _recentMeetings = [];
private List<CalendarItemWrapper> _nextCalendarItems = [];
private List<Note> _pinnedNotes = [];
private CancellationTokenSource? _cancellationTokenSource;
private bool _isDisposed = false;
private bool _hasStudents => _studentCount > 0;
private bool _hasTeams => _teamCount > 0;
protected override void OnInitialized()
{
_cancellationTokenSource = new CancellationTokenSource();
}
protected override async Task OnInitializedAsync()
{
await LoadStatistics();
await LoadPinnedNotes();
await LoadMeetingHistoryCount();
await LoadNextCalendarItem();
}
private async Task HandleNoteChanged()
{
if (!_isDisposed)
{
await LoadPinnedNotes();
StateHasChanged();
}
}
private async Task LoadPinnedNotes()
{
if (_isDisposed) return;
try
{
var cancellationToken = _cancellationTokenSource?.Token ?? CancellationToken.None;
_pinnedNotes = (await NotesService.GetPinnedNotesAsync()).ToList();
}
catch (TaskCanceledException)
{
// Component was disposed, ignore
}
catch (JSDisconnectedException)
{
// JS connection lost, ignore
}
catch (Exception)
{
// Error loading pinned notes - ignore
}
}
private async Task LoadStatistics()
{
// Events statistics
var events = await Context.Events.ToListAsync();
_eventCount = events.Count();
_individualEventsCount = events.Count(e => e.EventFormat == EventFormat.Individual);
_teamEventsCount = events.Count(e => e.EventFormat == EventFormat.Team);
// Students statistics
_studentCount = await Context.Students.CountAsync();
// Grade distribution
var gradeGroups = await Context.Students
.GroupBy(s => s.Grade)
.Select(g => new { Grade = g.Key, Count = g.Count() })
.OrderBy(g => g.Grade)
.ToListAsync();
if (gradeGroups.Any())
{
_gradeDistribution = string.Join(" | ", gradeGroups.Select(g =>
$"<span style=\"white-space: nowrap\">{AppIcons.GetOrdinalSuperscript(g.Grade)}: <strong>{g.Count}</strong></span>"));
}
else
{
_gradeDistribution = "No students yet";
}
// Teams statistics
var contextTeams = await Context.Teams.ToListAsync();
_teamCount = contextTeams.Count;
_individualTeamsCount = contextTeams.Count(e => e.Event.EventFormat == EventFormat.Individual);
_groupTeamsCount = contextTeams.Count(e => e.Event.EventFormat == EventFormat.Team);
}
private async Task LoadMeetingHistoryCount()
{
if (_isDisposed) return;
try
{
var meetingHistories = await TeamMeetingHistoryService.GetMeetingHistoriesAsync();
var historiesList = meetingHistories.ToList();
_meetingHistoryCount = historiesList.Count;
// Get the 3 most recent meetings in descending order
_recentMeetings = historiesList
.OrderByDescending(m => m.MeetingDate)
.ThenByDescending(m => m.Id)
.Take(3)
.ToList();
}
catch (TaskCanceledException)
{
// Component was disposed, ignore
}
catch (JSDisconnectedException)
{
// JS connection lost, ignore
}
catch (Exception)
{
// Error loading meeting history - ignore
_meetingHistoryCount = 0;
_recentMeetings = [];
}
}
private async Task ShowMeetingDetails(TeamMeetingHistory meeting)
{
var parameters = new DialogParameters
{
["MeetingHistoryId"] = meeting.Id
};
var options = new DialogOptions
{
CloseOnEscapeKey = true,
CloseButton = true,
MaxWidth = MaxWidth.Large,
FullWidth = true
};
await DialogService.ShowAsync<MeetingHistoryDetailDialog>("Meeting Details", parameters, options);
}
private async Task LoadNextCalendarItem()
{
if (_isDisposed) return;
try
{
_nextCalendarItems = await CalendarService.GetUpcomingCalendarItemsAsync(3);
}
catch (TaskCanceledException)
{
// Component was disposed, ignore
}
catch (JSDisconnectedException)
{
// JS connection lost, ignore
}
catch (Exception)
{
// Error loading calendar items - ignore
_nextCalendarItems = [];
}
}
private void NavigateToCalendar(string date)
{
Navigation.NavigateTo($"/calendar?date={date}");
}
public async ValueTask DisposeAsync()
{
if (!_isDisposed)
{
_isDisposed = true;
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
}
await ValueTask.CompletedTask;
}
}