Compare commits

...
6 Commits
Author SHA1 Message Date
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
poprhythm 84eaf338a9 Enhance TeamMeetingHistoryDialog to display team members' attendance
This commit updates the TeamMeetingHistoryDialog component to show the attendance status of team members during meetings. It replaces the previous status display with a list of team members, indicating their presence or absence using MudBlazor chips. Additionally, the TeamMeetingHistoryService is modified to include student data for accurate attendance tracking. These changes improve the clarity and usability of the meeting history dialog, enhancing the overall user experience in managing team meetings.
2026-01-27 20:10:40 -05:00
poprhythm 15d7edec8f Add Team Meeting History functionality with dialog and badge components
This commit introduces a new feature to display meeting history for teams. It adds a `TeamMeetingHistoryDialog` component that shows the meeting history in a dialog format, including a loading state and error handling. Additionally, a `TeamMeetingHistoryBadge` component is created to display the count of meetings for each team, enhancing the user interface with tooltips for better interaction. The `Details` and `Index` components are updated to integrate these new features, allowing users to view meeting history directly from the team details and index pages. These changes improve the overall functionality and user experience in managing team meetings.
2026-01-26 23:31:59 -05:00
poprhythm 46836fde2e Refactor Calendar component to adjust default meeting times and improve event item styling
This commit updates the Calendar component by changing the default start time for meetings to 9:00 AM and the end time to 5:01 PM, ensuring better alignment with typical work hours. Additionally, the styling of calendar event items is refined by removing fixed width and height properties, allowing for more flexible rendering. These changes enhance the usability and visual presentation of the calendar feature, contributing to an improved user experience.
2026-01-26 22:15:03 -05:00
poprhythm d0ce71397b Update Calendar component to use two-way binding for view and current day properties
This commit modifies the Calendar component to implement two-way binding for the View and CurrentDay properties of the MudCalendar. By using `@bind-View` and `@bind-CurrentDay`, the component enhances data synchronization between the UI and underlying state, improving user interaction and responsiveness. This change aligns with ongoing efforts to refine the user experience in the calendar feature.
2026-01-26 21:47:06 -05:00
poprhythm 840a8edbf1 Enhance Calendar component with improved event tooltip functionality and styling
This commit updates the Calendar component to include a new method for generating tooltips for both events and meetings, enhancing user interaction by providing contextual information. Additionally, CSS styles are introduced for calendar event items, improving their appearance and hover effects. These changes contribute to a more informative and visually appealing calendar experience, aligning with the ongoing efforts to refine the user interface.
2026-01-26 21:31:18 -05:00
17 changed files with 868 additions and 241 deletions
+43 -142
View File
@@ -5,9 +5,7 @@
@using Heron.MudCalendar @using Heron.MudCalendar
@using Microsoft.Extensions.Logging @using Microsoft.Extensions.Logging
@using WebApp.Authentication @using WebApp.Authentication
@using Core.Utility @inject ICalendarService CalendarService
@inject IEventOccurrenceService EventOccurrenceService
@inject ITeamMeetingHistoryService TeamMeetingHistoryService
@inject ILogger<Index> Logger @inject ILogger<Index> Logger
@inject IDialogService DialogService @inject IDialogService DialogService
@@ -37,29 +35,30 @@
<MudCalendar T="CalendarItemWrapper" <MudCalendar T="CalendarItemWrapper"
Items="_calendarItems" Items="_calendarItems"
View="_currentView" @bind-View="_currentView"
CurrentDay="@_calendarDate" @bind-CurrentDay="_calendarDate"
Class="event-calendar" Class="event-calendar"
ItemClicked="OnItemClicked"> ItemClicked="OnItemClicked">
<MonthTemplate> <MonthTemplate>
@* <MudTooltip Text="@GetEventTooltip(context)"> *@ <MudTooltip Text="@GetEventTooltip(context)">
<div class="d-flex gap-1"> <div class="calendar-event-item">
<MudIcon Icon="@Icons.Material.Filled.Circle" Color="Color.Secondary" Size="Size.Small"/>
<div>@context.Text</div>
</div>
@* </MudTooltip> *@
</MonthTemplate>
<WeekTemplate>
@* <MudTooltip Text="@GetEventTooltip(context)"> *@
<div style="width: 100%; height: 100%;">
@context.Text @context.Text
</div> </div>
@* </MudTooltip> *@ </MudTooltip>
</MonthTemplate>
<WeekTemplate>
<MudTooltip Text="@GetEventTooltip(context)">
<div class="calendar-event-item">
@context.Text
</div>
</MudTooltip>
</WeekTemplate> </WeekTemplate>
<DayTemplate> <DayTemplate>
@* <MudTooltip Text="@GetEventTooltip(context)"> *@ <MudTooltip Text="@GetEventTooltip(context)">
<div>@context.Text</div> <div class="calendar-event-item">
@* </MudTooltip> *@ @context.Text
</div>
</MudTooltip>
</DayTemplate> </DayTemplate>
</MudCalendar> </MudCalendar>
</MudStack> </MudStack>
@@ -71,8 +70,17 @@
private DateTime _calendarDate = DateTime.Today; private DateTime _calendarDate = DateTime.Today;
private CalendarView _currentView = CalendarView.Month; private CalendarView _currentView = CalendarView.Month;
[SupplyParameterFromQuery]
private string? Date { get; set; }
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
// Parse date from query parameter if provided
if (!string.IsNullOrEmpty(Date) && DateTime.TryParse(Date, out var parsedDate))
{
_calendarDate = parsedDate.Date;
}
await LoadCalendarEvents(); await LoadCalendarEvents();
} }
@@ -81,91 +89,8 @@
try try
{ {
Logger.LogInformation("Loading calendar events"); Logger.LogInformation("Loading calendar events");
var occurrences = await EventOccurrenceService.GetEventOccurrencesAsync(); _calendarItems = await CalendarService.GetAllCalendarItemsAsync();
Logger.LogInformation("Loaded {Count} calendar items", _calendarItems.Count);
var eventOccurrences = occurrences as EventOccurrence[] ?? occurrences.ToArray();
Logger.LogDebug("Received {Count} occurrences from service", eventOccurrences.Count());
// Get all unique event definition IDs that have occurrences
var eventDefinitionIds = eventOccurrences
.Where(occ => occ?.EventDefinition?.Id != null)
.Select(occ => occ!.EventDefinition!.Id)
.Distinct()
.ToList();
// Load teams for all event definitions
var teamsByEventId = await EventOccurrenceService.GetTeamsByEventDefinitionIdsAsync(eventDefinitionIds);
List<CalendarItemWrapper> items = [];
// Add event occurrences
foreach (var occ in eventOccurrences)
{
try
{
if (string.IsNullOrEmpty(occ.Name))
{
Logger.LogWarning("Occurrence with Id={Id} has null or empty Name", occ.Id);
}
// Get student first names for this event definition
var studentFirstNames = occ.EventDefinition != null && teamsByEventId.TryGetValue(occ.EventDefinition.Id, out var teams)
? TeamStudentNameFormatter.FormatStudentListForEvent(
occ.EventDefinition,
teams,
new TeamStudentNameFormatter.FormatOptions
{
CaptainIndicator = TeamStudentNameFormatter.CaptainIndicatorStyle.Star,
Ordering = TeamStudentNameFormatter.OrderingStyle.Alphabetical
})
: [];
var calendarEventItem = new CalendarEventItem(occ, studentFirstNames);
items.Add(new CalendarItemWrapper(calendarEventItem));
}
catch (Exception ex)
{
Logger.LogError(ex, "Error creating CalendarEventItem for occurrence Id={Id}, Name={Name}",
occ?.Id, occ?.Name);
// Continue processing other items
}
}
// Load and add meeting histories
try
{
Logger.LogInformation("Loading meeting histories");
var meetingHistories = await TeamMeetingHistoryService.GetMeetingHistoriesAsync();
foreach (var meetingHistory in meetingHistories)
{
try
{
var calendarMeetingItem = new CalendarMeetingItem(meetingHistory);
items.Add(new CalendarItemWrapper(calendarMeetingItem));
}
catch (Exception ex)
{
Logger.LogError(ex, "Error creating CalendarMeetingItem for meeting history Id={Id}, Date={Date}",
meetingHistory?.Id, meetingHistory?.MeetingDate);
// Continue processing other items
}
}
Logger.LogInformation("Added {Count} meeting histories to calendar", meetingHistories.Count());
}
catch (Exception ex)
{
Logger.LogError(ex, "Error loading meeting histories");
// Continue - don't fail the entire calendar load if meetings fail
}
_calendarItems = items;
Logger.LogInformation("Created {Count} calendar items from {OccurrenceCount} occurrences and meetings",
_calendarItems.Count, eventOccurrences.Count());
// Find the next date with events
_calendarDate = GetNextDateWithEvents();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -179,50 +104,18 @@
} }
private DateTime GetNextDateWithEvents()
{
try
{
if (_calendarItems == null || !_calendarItems.Any())
{
Logger.LogDebug("No calendar items available, returning today's date");
return DateTime.Today;
}
var today = DateTime.Today; private string GetEventTooltip(CalendarItemWrapper wrapper)
var nextItem = _calendarItems
.Where(item =>
{ {
try if (wrapper.ItemType == CalendarItemType.Event && wrapper.EventItem != null)
{ {
return item.Start.Date >= today; return GetEventTooltip(wrapper.EventItem);
} }
catch (Exception ex) else if (wrapper.ItemType == CalendarItemType.Meeting && wrapper.MeetingItem != null)
{ {
Logger.LogWarning(ex, "Error checking item date, skipping item"); return GetMeetingTooltip(wrapper.MeetingItem);
return false;
}
})
.OrderBy(item => item.Start)
.FirstOrDefault();
if (nextItem != null)
{
return nextItem.Start.Date;
}
// Fallback to first item if no future items
var firstItem = _calendarItems
.OrderBy(item => item.Start)
.FirstOrDefault();
return firstItem != null ? firstItem.Start.Date : DateTime.Today;
}
catch (Exception ex)
{
Logger.LogError(ex, "Error in GetNextDateWithEvents");
return DateTime.Today;
} }
return wrapper.Text;
} }
private string GetEventTooltip(CalendarEventItem item) private string GetEventTooltip(CalendarEventItem item)
@@ -257,6 +150,14 @@
return string.Join("\n", parts); return string.Join("\n", parts);
} }
private string GetMeetingTooltip(CalendarMeetingItem item)
{
if (item.MeetingHistoryData == null)
return "Team Meeting";
return $"Team Meeting\nDate: {item.MeetingHistoryData.MeetingDate:g}";
}
private async Task OnItemClicked(CalendarItemWrapper wrapper) private async Task OnItemClicked(CalendarItemWrapper wrapper)
{ {
if (_calendarItems == null) if (_calendarItems == null)
@@ -1,9 +1,5 @@
@namespace WebApp.Components.Features.MeetingSchedule @namespace WebApp.Components.Features.MeetingSchedule
@using Core.Entities
@using Core.Services @using Core.Services
@using Core.Utility
@using WebApp.Services
@using WebApp.Components.Shared.Components
@using WebApp.Models @using WebApp.Models
@inject ITeamMeetingHistoryService TeamMeetingHistoryService @inject ITeamMeetingHistoryService TeamMeetingHistoryService
@inject INotesService NotesService @inject INotesService NotesService
@@ -51,6 +47,8 @@
</MudTooltip> </MudTooltip>
</MudStack> </MudStack>
<MudGrid>
<MudItem xs="12" md="6">
<MudText Typo="Typo.subtitle1">Teams That Met (@_meetingHistory.Teams.Count)</MudText> <MudText Typo="Typo.subtitle1">Teams That Met (@_meetingHistory.Teams.Count)</MudText>
<MudPaper Elevation="1" Class="pa-2"> <MudPaper Elevation="1" Class="pa-2">
<MudStack Row="true" Spacing="1" Wrap="Wrap.Wrap"> <MudStack Row="true" Spacing="1" Wrap="Wrap.Wrap">
@@ -60,9 +58,8 @@
} }
</MudStack> </MudStack>
</MudPaper> </MudPaper>
</MudItem>
<MudDivider /> <MudItem xs="12" md="6">
<MudText Typo="Typo.subtitle1">Students (@GetAllStudentsFromTeams().Count)</MudText> <MudText Typo="Typo.subtitle1">Students (@GetAllStudentsFromTeams().Count)</MudText>
<MudPaper Elevation="1" Class="pa-2"> <MudPaper Elevation="1" Class="pa-2">
<MudStack Row="true" Spacing="1" Wrap="Wrap.Wrap"> <MudStack Row="true" Spacing="1" Wrap="Wrap.Wrap">
@@ -84,6 +81,8 @@
} }
</MudStack> </MudStack>
</MudPaper> </MudPaper>
</MudItem>
</MudGrid>
@if (_meetingNote != null) @if (_meetingNote != null)
{ {
@@ -33,6 +33,8 @@
<MudDivider /> <MudDivider />
<MudGrid>
<MudItem xs="12" md="6">
<MudPaper Elevation="1" Class="pa-2" Style="max-height: 300px; overflow-y: auto;"> <MudPaper Elevation="1" Class="pa-2" Style="max-height: 300px; overflow-y: auto;">
<TeamToggleSelector Teams="@AllTeams" <TeamToggleSelector Teams="@AllTeams"
SelectedTeams="_selectedTeams" SelectedTeams="_selectedTeams"
@@ -40,9 +42,8 @@
Title="Teams That Met" Title="Teams That Met"
ShowEventAttributes="false" /> ShowEventAttributes="false" />
</MudPaper> </MudPaper>
</MudItem>
<MudDivider /> <MudItem xs="12" md="6">
<MudPaper Elevation="1" Class="pa-2" Style="max-height: 300px; overflow-y: auto;"> <MudPaper Elevation="1" Class="pa-2" Style="max-height: 300px; overflow-y: auto;">
<StudentToggleSelector Students="@AllStudents" <StudentToggleSelector Students="@AllStudents"
SelectedStudents="_selectedStudents" SelectedStudents="_selectedStudents"
@@ -50,6 +51,8 @@
Title="Students Present" Title="Students Present"
ShowFullName="true" /> ShowFullName="true" />
</MudPaper> </MudPaper>
</MudItem>
</MudGrid>
<MudDivider /> <MudDivider />
@@ -149,11 +152,28 @@
// Match teams by ID to ensure reference equality with MudToggleGroup // Match teams by ID to ensure reference equality with MudToggleGroup
var selectedTeamIds = existingHistory.Teams.Select(t => t.Id).ToHashSet(); var selectedTeamIds = existingHistory.Teams.Select(t => t.Id).ToHashSet();
_selectedTeams = AllTeams.Where(t => selectedTeamIds.Contains(t.Id)); var matchedTeams = AllTeams.Where(t => selectedTeamIds.Contains(t.Id)).ToList();
// If we couldn't match all teams from AllTeams, use the teams from existing history
// This can happen if AllTeams is empty or doesn't contain all the teams
if (matchedTeams.Count != existingHistory.Teams.Count && AllTeams.Any())
{
// Try to match what we can, but log a warning
System.Diagnostics.Debug.WriteLine($"Warning: Could not match all teams. Expected {existingHistory.Teams.Count}, matched {matchedTeams.Count}");
}
_selectedTeams = matchedTeams.Any() ? matchedTeams : existingHistory.Teams;
// Match students by ID to ensure reference equality with MudToggleGroup // Match students by ID to ensure reference equality with MudToggleGroup
var selectedStudentIds = existingHistory.Students.Select(s => s.Id).ToHashSet(); var selectedStudentIds = existingHistory.Students.Select(s => s.Id).ToHashSet();
_selectedStudents = AllStudents.Where(s => selectedStudentIds.Contains(s.Id)); var matchedStudents = AllStudents.Where(s => selectedStudentIds.Contains(s.Id)).ToList();
// If we couldn't match all students from AllStudents, use the students from existing history
if (matchedStudents.Count != existingHistory.Students.Count && AllStudents.Any())
{
// Try to match what we can, but log a warning
System.Diagnostics.Debug.WriteLine($"Warning: Could not match all students. Expected {existingHistory.Students.Count}, matched {matchedStudents.Count}");
}
_selectedStudents = matchedStudents.Any() ? matchedStudents : existingHistory.Students;
// Load existing note if available // Load existing note if available
var existingNote = await TeamMeetingHistoryService.GetMeetingNoteAsync(existingHistory.MeetingDate); var existingNote = await TeamMeetingHistoryService.GetMeetingNoteAsync(existingHistory.MeetingDate);
@@ -247,6 +267,13 @@
return; return;
} }
// Validate that we have at least one team selected
if (!_selectedTeams.Any())
{
Snackbar.Add("Please select at least one team", Severity.Warning);
return;
}
// Check if a meeting history already exists for this date (only when creating new, not editing) // Check if a meeting history already exists for this date (only when creating new, not editing)
if (!_isEditMode) if (!_isEditMode)
{ {
@@ -344,10 +371,19 @@
return; return;
} }
meetingHistory = existingHistory; // Ensure we have teams and students - use existing if selected lists are empty (fallback)
meetingHistory.MeetingDate = _meetingDate.Value; var teamsToSave = _selectedTeams.Any() ? _selectedTeams.ToList() : existingHistory.Teams.ToList();
meetingHistory.Teams = _selectedTeams.ToList(); var studentsToSave = _selectedStudents.Any() ? _selectedStudents.ToList() : existingHistory.Students.ToList();
meetingHistory.Students = _selectedStudents.ToList();
// Create a new meeting history object with the updated data
// Use IDs to ensure we're working with the correct entities
meetingHistory = new TeamMeetingHistory
{
Id = existingHistory.Id,
MeetingDate = _meetingDate.Value,
Teams = teamsToSave,
Students = studentsToSave
};
await TeamMeetingHistoryService.UpdateMeetingHistoryAsync(meetingHistory); await TeamMeetingHistoryService.UpdateMeetingHistoryAsync(meetingHistory);
@@ -0,0 +1,108 @@
@namespace WebApp.Components.Features.Teams.Components
@using Core.Entities
@using WebApp.Services
@using MudBlazor
@inject ITeamMeetingHistoryService TeamMeetingHistoryService
@inject IDialogService DialogService
@implements IAsyncDisposable
@if (_meetingCount.HasValue && _meetingCount.Value > 0)
{
<MudTooltip Text="View Meeting History">
<MudBadge Content="@_meetingCount.Value.ToString()"
Color="Color.Primary"
Overlap="true">
<MudIconButton Icon="@Icons.Material.Filled.History"
Size="Size.Small"
Color="Color.Default"
OnClick="OpenMeetingHistoryDialog"
Variant="Variant.Text" />
</MudBadge>
</MudTooltip>
}
@code {
[Parameter]
public int TeamId { get; set; }
[Parameter]
public string TeamName { get; set; } = string.Empty;
private int? _meetingCount;
private CancellationTokenSource? _cancellationTokenSource;
private bool _isDisposed = false;
protected override void OnInitialized()
{
_cancellationTokenSource = new CancellationTokenSource();
}
protected override async Task OnInitializedAsync()
{
await LoadMeetingCount();
}
private async Task LoadMeetingCount()
{
if (_isDisposed) return;
try
{
var cancellationToken = _cancellationTokenSource?.Token ?? CancellationToken.None;
_meetingCount = await TeamMeetingHistoryService.GetMeetingHistoryCountForTeamAsync(TeamId);
}
catch (TaskCanceledException)
{
// Component was disposed, ignore
}
catch (JSDisconnectedException)
{
// JS connection lost, ignore
}
catch (Exception)
{
// Ignore errors - just don't show the badge
_meetingCount = null;
}
finally
{
if (!_isDisposed)
{
StateHasChanged();
}
}
}
private async Task OpenMeetingHistoryDialog()
{
if (_isDisposed) return;
var parameters = new DialogParameters
{
["TeamId"] = TeamId,
["TeamName"] = TeamName
};
var options = new DialogOptions
{
CloseOnEscapeKey = true,
CloseButton = true,
MaxWidth = MaxWidth.Medium,
FullWidth = true
};
await DialogService.ShowAsync<TeamMeetingHistoryDialog>($"{TeamName} Meeting History", parameters, options);
}
public async ValueTask DisposeAsync()
{
if (!_isDisposed)
{
_isDisposed = true;
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
}
await ValueTask.CompletedTask;
}
}
@@ -1,5 +1,6 @@
@using WebApp.Models @using WebApp.Models
@using Core.Utility @using Core.Utility
@using WebApp.Components.Features.Teams.Components
@if (Title != null) @if (Title != null)
{ {
@@ -28,6 +29,7 @@
@if (IsSelected(team)) @if (IsSelected(team))
{ {
var isExtended = IsExtended(team); var isExtended = IsExtended(team);
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudTooltip Text="@(isExtended ? "Remove from extended teams" : "Extend to 2 time slots")"> <MudTooltip Text="@(isExtended ? "Remove from extended teams" : "Extend to 2 time slots")">
<MudIconButton Icon="@(isExtended ? Icons.Material.Filled.AddCircle : Icons.Material.Filled.Add)" <MudIconButton Icon="@(isExtended ? Icons.Material.Filled.AddCircle : Icons.Material.Filled.Add)"
Size="Size.Small" Size="Size.Small"
@@ -36,6 +38,8 @@
OnClick="@(() => ToggleExtended(team))" OnClick="@(() => ToggleExtended(team))"
Style="margin-left: 4px; padding: 2px;" /> Style="margin-left: 4px; padding: 2px;" />
</MudTooltip> </MudTooltip>
<TeamMeetingHistoryBadge TeamId="@team.Id" TeamName="@team.ToString()" />
</MudStack>
} }
@if (ShowEventAttributes) @if (ShowEventAttributes)
{ {
@@ -3,9 +3,11 @@
@using Microsoft.EntityFrameworkCore @using Microsoft.EntityFrameworkCore
@using WebApp.Components.Shared.Components @using WebApp.Components.Shared.Components
@using WebApp.Components.Features.Teams.Components @using WebApp.Components.Features.Teams.Components
@using MudBlazor
@inject AppDbContext Context @inject AppDbContext Context
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IJSRuntime JSRuntime @inject IJSRuntime JSRuntime
@inject IDialogService DialogService
@if (Team is null) @if (Team is null)
{ {
@@ -29,6 +31,11 @@
Href="@($"/teams/edit?id={Team.Id}&returnUrl={ReturnUrl ?? "/teams"}")" Href="@($"/teams/edit?id={Team.Id}&returnUrl={ReturnUrl ?? "/teams"}")"
Variant="Variant.Outlined">Edit</MudButton> Variant="Variant.Outlined">Edit</MudButton>
</MudTooltip> </MudTooltip>
<MudTooltip Text="View Meeting History">
<MudButton StartIcon="@Icons.Material.Filled.History"
OnClick="ShowMeetingHistory"
Variant="Variant.Outlined">Meeting History</MudButton>
</MudTooltip>
</div> </div>
</ActionButtons> </ActionButtons>
</PageHeader> </PageHeader>
@@ -86,4 +93,26 @@
{ {
await JSRuntime.InvokeVoidAsync("window.print"); await JSRuntime.InvokeVoidAsync("window.print");
} }
private async Task ShowMeetingHistory()
{
if (Team == null) return;
var teamName = Team.ToString();
var parameters = new DialogParameters
{
["TeamId"] = Team.Id,
["TeamName"] = teamName
};
var options = new DialogOptions
{
CloseOnEscapeKey = true,
CloseButton = true,
MaxWidth = MaxWidth.Medium,
FullWidth = true
};
await DialogService.ShowAsync<TeamMeetingHistoryDialog>($"{teamName} Meeting History", parameters, options);
}
} }
@@ -52,6 +52,7 @@
@context.Item.ToString() @context.Item.ToString()
</MudLink> </MudLink>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1"> <MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<TeamMeetingHistoryBadge TeamId="@context.Item.Id" TeamName="@context.Item.ToString()" />
<IconButtonWithTooltip Icon="@Icons.Material.Filled.Edit" <IconButtonWithTooltip Icon="@Icons.Material.Filled.Edit"
TooltipText="Edit" TooltipText="Edit"
Href="@($"/teams/edit?id={context.Item.Id}&returnUrl=/teams")" /> Href="@($"/teams/edit?id={context.Item.Id}&returnUrl=/teams")" />
@@ -0,0 +1,151 @@
@namespace WebApp.Components.Features.Teams
@using Core.Entities
@using WebApp.Services
@using Microsoft.AspNetCore.Components
@using WebApp.Models
@using WebApp.Components.Shared.Components
@using Core.Utility
@inject ITeamMeetingHistoryService TeamMeetingHistoryService
@implements IAsyncDisposable
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">@TeamName Meeting History</MudText>
</TitleContent>
<DialogContent>
@if (_isLoading)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="my-4" />
}
else if (!_meetingHistories.Any())
{
<MudAlert Severity="Severity.Info">No meeting history found for this team.</MudAlert>
}
else
{
<MudTable Items="_meetingHistories" Hover="true" Dense="true">
<HeaderContent>
<MudTh>Date</MudTh>
<MudTh>Team Members</MudTh>
</HeaderContent>
<RowTemplate>
@{
var team = context.Teams.FirstOrDefault(t => t.Id == TeamId);
var presentStudentIds = context.Students.Select(s => s.Id).ToHashSet();
var absentStudents = team?.Students.Where(s => !presentStudentIds.Contains(s.Id)).ToList() ?? [];
}
<MudTd>@context.MeetingDate.ToString("MM/dd/yyyy")</MudTd>
<MudTd>
@if (team != null)
{
<MudStack Row="true" Spacing="1" Wrap="Wrap.Wrap" AlignItems="AlignItems.Center">
@foreach (var student in team.Students)
{
var isAbsent = absentStudents.Contains(student);
var formattedName = TeamStudentNameFormatter.FormatStudentName(
student,
team,
new TeamStudentNameFormatter.FormatOptions
{
MarkAbsent = true,
AbsentStudents = absentStudents
});
<MudChip T="string"
Size="Size.Small"
Color="Color.Default"
Variant="@AppIcons.StudentChipVariant()"
Style="@(isAbsent ? "opacity: 0.5;" : "")">
<span>@formattedName</span>
</MudChip>
}
</MudStack>
}
</MudTd>
</RowTemplate>
</MudTable>
}
</DialogContent>
<DialogActions>
<MudSpacer />
<MudButton OnClick="Close">Close</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = null!;
[Parameter]
public int TeamId { get; set; }
[Parameter]
public string TeamName { get; set; } = string.Empty;
private List<TeamMeetingHistory> _meetingHistories = [];
private bool _isLoading = true;
private CancellationTokenSource? _cancellationTokenSource;
private bool _isDisposed = false;
protected override void OnInitialized()
{
_cancellationTokenSource = new CancellationTokenSource();
}
protected override async Task OnInitializedAsync()
{
await LoadMeetingHistories();
}
private async Task LoadMeetingHistories()
{
if (_isDisposed) return;
try
{
var cancellationToken = _cancellationTokenSource?.Token ?? CancellationToken.None;
var histories = await TeamMeetingHistoryService.GetMeetingHistoriesForTeamAsync(TeamId);
_meetingHistories = histories.ToList();
}
catch (TaskCanceledException)
{
// Component was disposed, ignore
}
catch (JSDisconnectedException)
{
// JS connection lost, ignore
}
catch (Exception ex)
{
if (!_isDisposed)
{
System.Diagnostics.Debug.WriteLine($"Error loading meeting histories: {ex.Message}");
}
}
finally
{
if (!_isDisposed)
{
_isLoading = false;
StateHasChanged();
}
}
}
private void Close()
{
if (_isDisposed) return;
MudDialog.Close();
}
public async ValueTask DisposeAsync()
{
if (!_isDisposed)
{
_isDisposed = true;
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
}
await ValueTask.CompletedTask;
}
}
+170 -19
View File
@@ -5,10 +5,14 @@
@using Core.Entities @using Core.Entities
@using WebApp.Services @using WebApp.Services
@using WebApp.Components.Shared.Components @using WebApp.Components.Shared.Components
@using Heron.MudCalendar
@inject IConfiguration Configuration @inject IConfiguration Configuration
@inject AppDbContext Context @inject AppDbContext Context
@inject INotesService NotesService @inject INotesService NotesService
@inject IDialogService DialogService @inject IDialogService DialogService
@inject ITeamMeetingHistoryService TeamMeetingHistoryService
@inject ICalendarService CalendarService
@inject NavigationManager Navigation
@implements IAsyncDisposable @implements IAsyncDisposable
<PageTitle>@Configuration["ChapterSettings:Name"] - TSA Chapter Organizer</PageTitle> <PageTitle>@Configuration["ChapterSettings:Name"] - TSA Chapter Organizer</PageTitle>
@@ -124,13 +128,76 @@ else
<MudGrid> <MudGrid>
<DashboardCard Icon="@AppIcons.Scheduler" <DashboardCard Icon="@AppIcons.Scheduler"
Title="Meeting Schedule Planner" Title="Meeting Schedule Planner"
Caption="Optimize meeting times" NavigateUrl="/meeting-schedule">
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" <DashboardCard Icon="@AppIcons.EventCalendar"
Title="Event Calendar" Title="Event Calendar"
Caption="Conference schedules" NavigateUrl="/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> </MudGrid>
<MudPaper Elevation="0" Class="my-4"> <MudPaper Elevation="0" Class="my-4">
@@ -150,21 +217,6 @@ else
NavigateUrl="/students/teams"/> NavigateUrl="/students/teams"/>
</MudGrid> </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>
<MudPaper Elevation="0" Class="my-4"> <MudPaper Elevation="0" Class="my-4">
<MudText Typo="Typo.h4">Chapter Data</MudText> <MudText Typo="Typo.h4">Chapter Data</MudText>
</MudPaper> </MudPaper>
@@ -189,6 +241,21 @@ else
Caption="@($"{_teamEventsCount} Team | {_individualEventsCount} Individual")" Caption="@($"{_teamEventsCount} Team | {_individualEventsCount} Individual")"
NavigateUrl="/events" /> NavigateUrl="/events" />
</MudGrid> </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 { @code {
@@ -200,6 +267,9 @@ else
private int _teamCount; private int _teamCount;
private int _individualTeamsCount; private int _individualTeamsCount;
private int _groupTeamsCount; private int _groupTeamsCount;
private int _meetingHistoryCount;
private List<TeamMeetingHistory> _recentMeetings = [];
private List<CalendarItemWrapper> _nextCalendarItems = [];
private List<Note> _pinnedNotes = []; private List<Note> _pinnedNotes = [];
private CancellationTokenSource? _cancellationTokenSource; private CancellationTokenSource? _cancellationTokenSource;
private bool _isDisposed = false; private bool _isDisposed = false;
@@ -216,6 +286,8 @@ else
{ {
await LoadStatistics(); await LoadStatistics();
await LoadPinnedNotes(); await LoadPinnedNotes();
await LoadMeetingHistoryCount();
await LoadNextCalendarItem();
} }
private async Task HandleNoteChanged() private async Task HandleNoteChanged()
@@ -285,6 +357,85 @@ else
_groupTeamsCount = contextTeams.Count(e => e.Event.EventFormat == EventFormat.Team); _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() public async ValueTask DisposeAsync()
{ {
if (!_isDisposed) if (!_isDisposed)
@@ -18,16 +18,16 @@
<MudNavLink Href="/students/teams" Icon="@AppIcons.Registration">Registration</MudNavLink> <MudNavLink Href="/students/teams" Icon="@AppIcons.Registration">Registration</MudNavLink>
</MudNavGroup> </MudNavGroup>
<MudNavGroup Title="Team Building" Icon="@Icons.Material.Filled.GroupAdd" Expanded="false">
<MudNavLink Href="/students/event-ranking" Icon="@AppIcons.EventRank">Event Ranking</MudNavLink>
<MudNavLink Href="/teams/assignment" Icon="@AppIcons.TeamAssignment">Team Assignment</MudNavLink>
</MudNavGroup>
<MudNavGroup Title="Chapter Data" Icon="@Icons.Material.Filled.Storage" Expanded="false"> <MudNavGroup Title="Chapter Data" Icon="@Icons.Material.Filled.Storage" Expanded="false">
<MudNavLink Href="/students" Icon="@Icons.Material.Filled.People">Students</MudNavLink> <MudNavLink Href="/students" Icon="@Icons.Material.Filled.People">Students</MudNavLink>
<MudNavLink Href="/events" Icon="@AppIcons.Events">Events</MudNavLink> <MudNavLink Href="/events" Icon="@AppIcons.Events">Events</MudNavLink>
</MudNavGroup> </MudNavGroup>
<MudNavGroup Title="Team Building" Icon="@Icons.Material.Filled.GroupAdd" Expanded="false">
<MudNavLink Href="/students/event-ranking" Icon="@AppIcons.EventRank">Event Ranking</MudNavLink>
<MudNavLink Href="/teams/assignment" Icon="@AppIcons.TeamAssignment">Team Assignment</MudNavLink>
</MudNavGroup>
<MudNavGroup Title="Tools" Icon="@Icons.Material.Filled.Build" Expanded="false"> <MudNavGroup Title="Tools" Icon="@Icons.Material.Filled.Build" Expanded="false">
<MudNavLink Href="/notes" Icon="@Icons.Material.Filled.Note">Notes</MudNavLink> <MudNavLink Href="/notes" Icon="@Icons.Material.Filled.Note">Notes</MudNavLink>
</MudNavGroup> </MudNavGroup>
+4 -4
View File
@@ -30,9 +30,9 @@ public class CalendarMeetingItem : CalendarItem
MeetingHistoryData = meetingHistory; MeetingHistoryData = meetingHistory;
// Set base class properties that the calendar component uses // Set base class properties that the calendar component uses
Text = "Team Meeting"; Text = "Team Meeting";
// Use meeting date at 3:00 PM as default time // Set start to 9:00 AM on the meeting date
Start = meetingHistory.MeetingDate.Date.AddHours(15); Start = meetingHistory.MeetingDate.Date.AddHours(9);
// Default to 1 hour duration // Set end to 5:00 PM on the meeting date (5:01 PM to ensure it displays until 5pm if end is exclusive)
End = Start.AddHours(1); End = meetingHistory.MeetingDate.Date.AddHours(17).AddMinutes(1);
} }
} }
+1
View File
@@ -195,6 +195,7 @@ builder.Services.AddScoped<WebApp.Services.FormValidationService>();
builder.Services.AddScoped<WebApp.Services.EventDefinitionService>(); builder.Services.AddScoped<WebApp.Services.EventDefinitionService>();
builder.Services.AddScoped<WebApp.Services.INotesService, WebApp.Services.NotesService>(); builder.Services.AddScoped<WebApp.Services.INotesService, WebApp.Services.NotesService>();
builder.Services.AddScoped<WebApp.Services.ITeamMeetingHistoryService, WebApp.Services.TeamMeetingHistoryService>(); builder.Services.AddScoped<WebApp.Services.ITeamMeetingHistoryService, WebApp.Services.TeamMeetingHistoryService>();
builder.Services.AddScoped<WebApp.Services.ICalendarService, WebApp.Services.CalendarService>();
builder.Services.AddScoped<Core.Services.INoteNamingService, Core.Services.NoteNamingService>(); builder.Services.AddScoped<Core.Services.INoteNamingService, Core.Services.NoteNamingService>();
builder.Services.AddScoped<WebApp.Services.MarkdownTablePasteService>(); builder.Services.AddScoped<WebApp.Services.MarkdownTablePasteService>();
builder.Services.AddScoped<WebApp.Services.IMeetingScheduleStateService, WebApp.Services.MeetingScheduleStateService>(); builder.Services.AddScoped<WebApp.Services.IMeetingScheduleStateService, WebApp.Services.MeetingScheduleStateService>();
+144
View File
@@ -0,0 +1,144 @@
using Core.Entities;
using Core.Utility;
using WebApp.Models;
namespace WebApp.Services;
/// <summary>
/// Service for calendar-related operations.
/// </summary>
public class CalendarService : ICalendarService
{
private readonly IEventOccurrenceService _eventOccurrenceService;
private readonly ITeamMeetingHistoryService _teamMeetingHistoryService;
public CalendarService(
IEventOccurrenceService eventOccurrenceService,
ITeamMeetingHistoryService teamMeetingHistoryService)
{
_eventOccurrenceService = eventOccurrenceService;
_teamMeetingHistoryService = teamMeetingHistoryService;
}
public async Task<List<CalendarItemWrapper>> GetAllCalendarItemsAsync()
{
var items = new List<CalendarItemWrapper>();
await AddEventItemsAsync(items, includeStudentNames: true);
await AddMeetingItemsAsync(items);
return items;
}
public async Task<List<CalendarItemWrapper>> GetUpcomingCalendarItemsAsync(int count = 3)
{
var today = DateTime.Today;
var items = new List<CalendarItemWrapper>();
await AddEventItemsAsync(items, startDate: today, includeStudentNames: false);
await AddMeetingItemsAsync(items, startDate: today);
// Sort by date and take top N
return items
.OrderBy(item => item.Start)
.Take(count)
.ToList();
}
private async Task AddEventItemsAsync(
List<CalendarItemWrapper> items,
DateTime? startDate = null,
bool includeStudentNames = false)
{
try
{
var occurrences = await _eventOccurrenceService.GetEventOccurrencesAsync();
var eventOccurrences = occurrences as EventOccurrence[] ?? occurrences.ToArray();
// Filter by start date if provided
if (startDate.HasValue)
{
eventOccurrences = eventOccurrences
.Where(occ => occ.StartTime.Date >= startDate.Value)
.ToArray();
}
Dictionary<int, List<Team>>? teamsByEventId = null;
if (includeStudentNames)
{
// Get all unique event definition IDs that have occurrences
var eventDefinitionIds = eventOccurrences
.Where(occ => occ?.EventDefinition?.Id != null)
.Select(occ => occ!.EventDefinition!.Id)
.Distinct()
.ToList();
// Load teams for all event definitions
teamsByEventId = await _eventOccurrenceService.GetTeamsByEventDefinitionIdsAsync(eventDefinitionIds);
}
// Add event occurrences
foreach (var occ in eventOccurrences)
{
try
{
List<string>? studentFirstNames = null;
if (includeStudentNames && occ.EventDefinition != null && teamsByEventId != null)
{
studentFirstNames = teamsByEventId.TryGetValue(occ.EventDefinition.Id, out var teams)
? TeamStudentNameFormatter.FormatStudentListForEvent(
occ.EventDefinition,
teams,
new TeamStudentNameFormatter.FormatOptions
{
CaptainIndicator = TeamStudentNameFormatter.CaptainIndicatorStyle.Star,
Ordering = TeamStudentNameFormatter.OrderingStyle.Alphabetical
})
: [];
}
var calendarEventItem = new CalendarEventItem(occ, studentFirstNames);
items.Add(new CalendarItemWrapper(calendarEventItem));
}
catch (Exception)
{
// Continue processing other items
}
}
}
catch (Exception)
{
// Continue - don't fail the entire calendar load if events fail
}
}
private async Task AddMeetingItemsAsync(
List<CalendarItemWrapper> items,
DateTime? startDate = null)
{
try
{
var meetingHistories = await _teamMeetingHistoryService.GetMeetingHistoriesAsync();
IEnumerable<TeamMeetingHistory> meetings = meetingHistories;
if (startDate.HasValue)
{
meetings = meetings.Where(m => m.MeetingDate.Date >= startDate.Value);
}
foreach (var meetingHistory in meetings)
{
try
{
var calendarMeetingItem = new CalendarMeetingItem(meetingHistory);
items.Add(new CalendarItemWrapper(calendarMeetingItem));
}
catch (Exception)
{
// Continue processing other items
}
}
}
catch (Exception)
{
// Continue - don't fail the entire calendar load if meetings fail
}
}
}
+25
View File
@@ -0,0 +1,25 @@
using Core.Entities;
using WebApp.Models;
namespace WebApp.Services;
/// <summary>
/// Service for calendar-related operations.
/// </summary>
public interface ICalendarService
{
/// <summary>
/// Gets all calendar items (events and meetings) for display in the calendar.
/// Includes student names for events.
/// </summary>
/// <returns>A list of CalendarItemWrapper objects ready for display.</returns>
Task<List<CalendarItemWrapper>> GetAllCalendarItemsAsync();
/// <summary>
/// Gets the next N upcoming calendar items (events and meetings) starting from today.
/// Returns CalendarItemWrapper objects ready for display.
/// </summary>
/// <param name="count">The maximum number of items to return. Default is 3.</param>
/// <returns>A list of CalendarItemWrapper objects ready for display.</returns>
Task<List<CalendarItemWrapper>> GetUpcomingCalendarItemsAsync(int count = 3);
}
@@ -47,4 +47,19 @@ public interface ITeamMeetingHistoryService
/// <param name="meetingDate">The date of the meeting</param> /// <param name="meetingDate">The date of the meeting</param>
/// <returns>The note if found, null otherwise</returns> /// <returns>The note if found, null otherwise</returns>
Task<Note?> GetMeetingNoteAsync(DateTime meetingDate); Task<Note?> GetMeetingNoteAsync(DateTime meetingDate);
/// <summary>
/// Gets all meeting histories for a specific team, ordered by date descending.
/// </summary>
/// <param name="teamId">The ID of the team</param>
/// <returns>Meeting histories for the team, ordered by date descending</returns>
Task<IEnumerable<TeamMeetingHistory>> GetMeetingHistoriesForTeamAsync(int teamId);
/// <summary>
/// Gets the count of meeting histories for a specific team.
/// This is more efficient than loading all histories just to count them.
/// </summary>
/// <param name="teamId">The ID of the team</param>
/// <returns>The number of meeting histories for the team</returns>
Task<int> GetMeetingHistoryCountForTeamAsync(int teamId);
} }
@@ -202,4 +202,27 @@ public class TeamMeetingHistoryService : ITeamMeetingHistoryService
.Distinct() .Distinct()
.ToListAsync(); .ToListAsync();
} }
public async Task<IEnumerable<TeamMeetingHistory>> GetMeetingHistoriesForTeamAsync(int teamId)
{
return await _context.TeamMeetingHistories
.AsNoTracking()
.Include(tmh => tmh.Teams)
.ThenInclude(t => t.Event)
.Include(tmh => tmh.Teams)
.ThenInclude(t => t.Students)
.Include(tmh => tmh.Students)
.Where(tmh => tmh.Teams.Any(t => t.Id == teamId))
.OrderByDescending(tmh => tmh.MeetingDate)
.ThenByDescending(tmh => tmh.Id)
.ToListAsync();
}
public async Task<int> GetMeetingHistoryCountForTeamAsync(int teamId)
{
return await _context.TeamMeetingHistories
.AsNoTracking()
.Where(tmh => tmh.Teams.Any(t => t.Id == teamId))
.CountAsync();
}
} }
+39
View File
@@ -266,3 +266,42 @@
.note-color-2 { .note-color-2 {
background-color: #f3e5f5; background-color: #f3e5f5;
} }
/* Calendar event item styling */
.calendar-event-item {
background-color: var(--mud-palette-primary);
color: var(--mud-palette-primary-text);
padding: 4px 8px;
border-radius: 4px;
font-size: 0.875rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
transition: background-color 0.2s ease, box-shadow 0.2s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
width: 100% !important;
height: 100% !important;
display: block !important;
box-sizing: border-box;
text-align: center;
}
/* Ensure tooltip wrapper doesn't constrain width or height and adds spacing */
.event-calendar .mud-tooltip-root {
width: 100%;
height: 100%;
display: block;
padding: 2px;
box-sizing: border-box;
}
.calendar-event-item:hover {
background-color: var(--mud-palette-primary-darken);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.16);
}
.calendar-event-item:active {
background-color: var(--mud-palette-primary-darken);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}