Add TeamMeetingHistory entity, service, and UI components for meeting history management
This commit introduces the TeamMeetingHistory entity, including its configuration and database migrations. A new ITeamMeetingHistoryService interface and its implementation, TeamMeetingHistoryService, are added to handle CRUD operations for meeting histories. Additionally, UI components such as History.razor, MeetingHistoryDetailDialog, and SaveMeetingHistoryDialog are created to facilitate viewing and saving meeting histories. The integration of INoteNamingService enhances note management for meeting records, improving overall functionality and user experience in the application.
This commit is contained in:
@@ -19,7 +19,7 @@ public interface INotesService
|
||||
/// Gets a page-specific note by page identifier.
|
||||
/// </summary>
|
||||
/// <param name="pageIdentifier">The page identifier (e.g., "Teams", "Registration")</param>
|
||||
/// <returns>The note with title "@{pageIdentifier}" or null if not found</returns>
|
||||
/// <returns>The note with title "#{pageIdentifier}" or null if not found</returns>
|
||||
Task<Note?> GetPageNoteAsync(string pageIdentifier);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using Core.Entities;
|
||||
|
||||
namespace WebApp.Services;
|
||||
|
||||
public interface ITeamMeetingHistoryService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all meeting histories within an optional date range.
|
||||
/// </summary>
|
||||
/// <param name="startDate">Optional start date filter</param>
|
||||
/// <param name="endDate">Optional end date filter</param>
|
||||
Task<IEnumerable<TeamMeetingHistory>> GetMeetingHistoriesAsync(DateTime? startDate = null, DateTime? endDate = null);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a specific meeting history by ID with teams and students included.
|
||||
/// </summary>
|
||||
Task<TeamMeetingHistory?> GetMeetingHistoryAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new meeting history record.
|
||||
/// </summary>
|
||||
Task<TeamMeetingHistory> CreateMeetingHistoryAsync(TeamMeetingHistory meetingHistory);
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing meeting history record.
|
||||
/// </summary>
|
||||
Task<TeamMeetingHistory> UpdateMeetingHistoryAsync(TeamMeetingHistory meetingHistory);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a meeting history record.
|
||||
/// </summary>
|
||||
Task DeleteMeetingHistoryAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets which teams met on a specific date.
|
||||
/// </summary>
|
||||
Task<IEnumerable<Team>> GetTeamsForDateAsync(DateTime date);
|
||||
|
||||
/// <summary>
|
||||
/// Gets which students were present on a specific date.
|
||||
/// </summary>
|
||||
Task<IEnumerable<Student>> GetStudentsForDateAsync(DateTime date);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the meeting note for a specific meeting date by looking it up by title.
|
||||
/// </summary>
|
||||
/// <param name="meetingDate">The date of the meeting</param>
|
||||
/// <returns>The note if found, null otherwise</returns>
|
||||
Task<Note?> GetMeetingNoteAsync(DateTime meetingDate);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Core.Entities;
|
||||
using Core.Services;
|
||||
using Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Security.Claims;
|
||||
@@ -10,15 +11,18 @@ public class NotesService : INotesService
|
||||
private readonly AppDbContext _context;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly ILogger<NotesService> _logger;
|
||||
private readonly INoteNamingService _noteNamingService;
|
||||
|
||||
public NotesService(
|
||||
AppDbContext context,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<NotesService> logger)
|
||||
ILogger<NotesService> logger,
|
||||
INoteNamingService noteNamingService)
|
||||
{
|
||||
_context = context;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_logger = logger;
|
||||
_noteNamingService = noteNamingService;
|
||||
}
|
||||
|
||||
private string? GetCurrentUserEmail()
|
||||
@@ -42,7 +46,7 @@ public class NotesService : INotesService
|
||||
}
|
||||
|
||||
return await query
|
||||
.OrderBy(n => n.Title.StartsWith("@") ? 1 : 0) // Non-page notes first (0), page notes last (1)
|
||||
.OrderBy(n => n.Title != null && n.Title.StartsWith("#") ? 1 : 0) // Non-page notes first (0), page notes last (1)
|
||||
.ThenByDescending(n => n.UpdatedAt) // Within each group, order by most recently updated
|
||||
.ToListAsync();
|
||||
}
|
||||
@@ -56,7 +60,7 @@ public class NotesService : INotesService
|
||||
|
||||
public async Task<Note?> GetPageNoteAsync(string pageIdentifier)
|
||||
{
|
||||
var pageNoteTitle = $"@{pageIdentifier}";
|
||||
var pageNoteTitle = _noteNamingService.GetPageNoteTitle(pageIdentifier);
|
||||
return await _context.Notes
|
||||
.AsNoTracking()
|
||||
.Where(n => n.Title == pageNoteTitle && !n.IsDeleted)
|
||||
@@ -184,7 +188,7 @@ public class NotesService : INotesService
|
||||
{
|
||||
return await _context.Notes
|
||||
.AsNoTracking()
|
||||
.Where(n => n.IsPinned && !n.Title.StartsWith("@") && !n.IsDeleted)
|
||||
.Where(n => n.IsPinned && (n.Title == null || !n.Title.StartsWith("#")) && !n.IsDeleted)
|
||||
.OrderByDescending(n => n.UpdatedAt)
|
||||
.Take(3)
|
||||
.ToListAsync();
|
||||
@@ -201,7 +205,7 @@ public class NotesService : INotesService
|
||||
}
|
||||
|
||||
// Prevent pinning page notes
|
||||
if (note.Title.StartsWith("@"))
|
||||
if (_noteNamingService.IsPageNote(note.Title))
|
||||
{
|
||||
throw new InvalidOperationException("Page notes cannot be pinned.");
|
||||
}
|
||||
@@ -213,13 +217,13 @@ public class NotesService : INotesService
|
||||
if (!note.IsPinned)
|
||||
{
|
||||
var pinnedCount = await _context.Notes
|
||||
.CountAsync(n => n.IsPinned && !n.Title.StartsWith("@") && !n.IsDeleted);
|
||||
.CountAsync(n => n.IsPinned && (n.Title == null || !n.Title.StartsWith("#")) && !n.IsDeleted);
|
||||
|
||||
if (pinnedCount >= 3)
|
||||
{
|
||||
// Unpin the oldest pinned note
|
||||
var oldestPinned = await _context.Notes
|
||||
.Where(n => n.IsPinned && !n.Title.StartsWith("@") && !n.IsDeleted)
|
||||
.Where(n => n.IsPinned && (n.Title == null || !n.Title.StartsWith("#")) && !n.IsDeleted)
|
||||
.OrderBy(n => n.UpdatedAt)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
@@ -250,7 +254,7 @@ public class NotesService : INotesService
|
||||
return await _context.Notes
|
||||
.AsNoTracking()
|
||||
.Where(n => n.IsDeleted)
|
||||
.OrderBy(n => n.Title.StartsWith("@") ? 1 : 0) // Non-page notes first (0), page notes last (1)
|
||||
.OrderBy(n => n.Title != null && n.Title.StartsWith("#") ? 1 : 0) // Non-page notes first (0), page notes last (1)
|
||||
.ThenByDescending(n => n.UpdatedAt) // Within each group, order by most recently updated
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
using Core.Entities;
|
||||
using Core.Services;
|
||||
using Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace WebApp.Services;
|
||||
|
||||
public class TeamMeetingHistoryService : ITeamMeetingHistoryService
|
||||
{
|
||||
private readonly AppDbContext _context;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly ILogger<TeamMeetingHistoryService> _logger;
|
||||
private readonly INotesService _notesService;
|
||||
private readonly INoteNamingService _noteNamingService;
|
||||
|
||||
public TeamMeetingHistoryService(
|
||||
AppDbContext context,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<TeamMeetingHistoryService> logger,
|
||||
INotesService notesService,
|
||||
INoteNamingService noteNamingService)
|
||||
{
|
||||
_context = context;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_logger = logger;
|
||||
_notesService = notesService;
|
||||
_noteNamingService = noteNamingService;
|
||||
}
|
||||
|
||||
private string? GetCurrentUserEmail()
|
||||
{
|
||||
var user = _httpContextAccessor.HttpContext?.User;
|
||||
if (user == null)
|
||||
return null;
|
||||
|
||||
return user.FindFirstValue(ClaimTypes.Email) ?? user.Identity?.Name;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TeamMeetingHistory>> GetMeetingHistoriesAsync(DateTime? startDate = null, DateTime? endDate = null)
|
||||
{
|
||||
var query = _context.TeamMeetingHistories
|
||||
.AsNoTracking()
|
||||
.Include(tmh => tmh.Teams)
|
||||
.ThenInclude(t => t.Event)
|
||||
.Include(tmh => tmh.Students)
|
||||
.AsQueryable();
|
||||
|
||||
if (startDate.HasValue)
|
||||
{
|
||||
var start = startDate.Value.Date;
|
||||
query = query.Where(tmh => tmh.MeetingDate >= start);
|
||||
}
|
||||
|
||||
if (endDate.HasValue)
|
||||
{
|
||||
var end = endDate.Value.Date.AddDays(1); // Include the entire end date
|
||||
query = query.Where(tmh => tmh.MeetingDate < end);
|
||||
}
|
||||
|
||||
return await query
|
||||
.OrderByDescending(tmh => tmh.MeetingDate)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<TeamMeetingHistory?> GetMeetingHistoryAsync(int id)
|
||||
{
|
||||
return await _context.TeamMeetingHistories
|
||||
.Include(tmh => tmh.Teams)
|
||||
.ThenInclude(t => t.Event)
|
||||
.Include(tmh => tmh.Teams)
|
||||
.ThenInclude(t => t.Students)
|
||||
.Include(tmh => tmh.Students)
|
||||
.FirstOrDefaultAsync(tmh => tmh.Id == id);
|
||||
}
|
||||
|
||||
public async Task<TeamMeetingHistory> CreateMeetingHistoryAsync(TeamMeetingHistory meetingHistory)
|
||||
{
|
||||
// Create a new meeting history entity to avoid tracking conflicts
|
||||
var newMeetingHistory = new TeamMeetingHistory
|
||||
{
|
||||
MeetingDate = meetingHistory.MeetingDate.Date // Normalize to date only
|
||||
};
|
||||
|
||||
// Attach teams by loading them from the database to avoid tracking conflicts
|
||||
var teamIds = meetingHistory.Teams.Select(t => t.Id).ToList();
|
||||
var teams = await _context.Teams
|
||||
.Where(t => teamIds.Contains(t.Id))
|
||||
.ToListAsync();
|
||||
|
||||
// Attach students by loading them from the database to avoid tracking conflicts
|
||||
var studentIds = meetingHistory.Students.Select(s => s.Id).ToList();
|
||||
var students = await _context.Students
|
||||
.Where(s => studentIds.Contains(s.Id))
|
||||
.ToListAsync();
|
||||
|
||||
// Attach teams and students to the new meeting history
|
||||
newMeetingHistory.Teams = teams;
|
||||
newMeetingHistory.Students = students;
|
||||
|
||||
_context.TeamMeetingHistories.Add(newMeetingHistory);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Meeting history created: {MeetingHistoryId} for date {MeetingDate}",
|
||||
newMeetingHistory.Id, newMeetingHistory.MeetingDate);
|
||||
|
||||
return newMeetingHistory;
|
||||
}
|
||||
|
||||
public async Task<TeamMeetingHistory> UpdateMeetingHistoryAsync(TeamMeetingHistory meetingHistory)
|
||||
{
|
||||
var existingHistory = await _context.TeamMeetingHistories
|
||||
.Include(tmh => tmh.Teams)
|
||||
.Include(tmh => tmh.Students)
|
||||
.FirstOrDefaultAsync(tmh => tmh.Id == meetingHistory.Id);
|
||||
|
||||
if (existingHistory == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Meeting history with ID {meetingHistory.Id} not found.");
|
||||
}
|
||||
|
||||
// Update properties
|
||||
existingHistory.MeetingDate = meetingHistory.MeetingDate.Date; // Normalize to date only
|
||||
|
||||
// Update teams
|
||||
existingHistory.Teams.Clear();
|
||||
foreach (var team in meetingHistory.Teams)
|
||||
{
|
||||
var existingTeam = await _context.Teams.FindAsync(team.Id);
|
||||
if (existingTeam != null)
|
||||
{
|
||||
existingHistory.Teams.Add(existingTeam);
|
||||
}
|
||||
}
|
||||
|
||||
// Update students
|
||||
existingHistory.Students.Clear();
|
||||
foreach (var student in meetingHistory.Students)
|
||||
{
|
||||
var existingStudent = await _context.Students.FindAsync(student.Id);
|
||||
if (existingStudent != null)
|
||||
{
|
||||
existingHistory.Students.Add(existingStudent);
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Meeting history updated: {MeetingHistoryId} by {User}",
|
||||
meetingHistory.Id, GetCurrentUserEmail());
|
||||
|
||||
return existingHistory;
|
||||
}
|
||||
|
||||
public async Task DeleteMeetingHistoryAsync(int id)
|
||||
{
|
||||
var meetingHistory = await _context.TeamMeetingHistories
|
||||
.FirstOrDefaultAsync(tmh => tmh.Id == id);
|
||||
|
||||
if (meetingHistory == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Meeting history with ID {id} not found.");
|
||||
}
|
||||
|
||||
_context.TeamMeetingHistories.Remove(meetingHistory);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Meeting history deleted: {MeetingHistoryId}", id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the meeting note for a specific meeting date by looking it up by title.
|
||||
/// </summary>
|
||||
/// <param name="meetingDate">The date of the meeting</param>
|
||||
/// <returns>The note if found, null otherwise</returns>
|
||||
public async Task<Note?> GetMeetingNoteAsync(DateTime meetingDate)
|
||||
{
|
||||
var noteTitle = _noteNamingService.GetMeetingNoteTitle(meetingDate);
|
||||
var notes = await _notesService.GetNotesAsync(includeDeleted: false);
|
||||
return notes.FirstOrDefault(n => n.Title == noteTitle);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Team>> GetTeamsForDateAsync(DateTime date)
|
||||
{
|
||||
var normalizedDate = date.Date;
|
||||
return await _context.TeamMeetingHistories
|
||||
.AsNoTracking()
|
||||
.Where(tmh => tmh.MeetingDate == normalizedDate)
|
||||
.SelectMany(tmh => tmh.Teams)
|
||||
.Include(t => t.Event)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Student>> GetStudentsForDateAsync(DateTime date)
|
||||
{
|
||||
var normalizedDate = date.Date;
|
||||
return await _context.TeamMeetingHistories
|
||||
.AsNoTracking()
|
||||
.Where(tmh => tmh.MeetingDate == normalizedDate)
|
||||
.SelectMany(tmh => tmh.Students)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user