Files
chapter-organizer/WebApp/Services/NotesService.cs
T
poprhythm 6bc4c2e7f2 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.
2026-01-19 22:02:59 -05:00

303 lines
9.2 KiB
C#

using Core.Entities;
using Core.Services;
using Data;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
namespace WebApp.Services;
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,
INoteNamingService noteNamingService)
{
_context = context;
_httpContextAccessor = httpContextAccessor;
_logger = logger;
_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<Note>> GetNotesAsync(bool includeDeleted = false)
{
var query = _context.Notes
.AsNoTracking()
.AsQueryable();
if (!includeDeleted)
{
query = query.Where(n => !n.IsDeleted);
}
return await query
.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();
}
public async Task<Note?> GetNoteAsync(int id)
{
return await _context.Notes
.Include(n => n.NoteHistories.OrderByDescending(h => h.ModifiedAt))
.FirstOrDefaultAsync(n => n.Id == id);
}
public async Task<Note?> GetPageNoteAsync(string pageIdentifier)
{
var pageNoteTitle = _noteNamingService.GetPageNoteTitle(pageIdentifier);
return await _context.Notes
.AsNoTracking()
.Where(n => n.Title == pageNoteTitle && !n.IsDeleted)
.FirstOrDefaultAsync();
}
public async Task<IEnumerable<NoteHistory>> GetNoteHistoryAsync(int noteId)
{
return await _context.NoteHistories
.AsNoTracking()
.Where(h => h.NoteId == noteId)
.OrderByDescending(h => h.ModifiedAt)
.Take(10)
.ToListAsync();
}
public async Task<Note> CreateNoteAsync(Note note)
{
var userEmail = GetCurrentUserEmail();
var now = DateTime.UtcNow;
note.CreatedAt = now;
note.UpdatedAt = now;
note.CreatedBy = userEmail;
note.LastModifiedBy = userEmail;
_context.Notes.Add(note);
await _context.SaveChangesAsync();
// Create initial history entry
var history = new NoteHistory
{
NoteId = note.Id,
Title = note.Title,
Content = note.Content,
ModifiedBy = userEmail,
ModifiedAt = now,
ChangeType = "Created"
};
_context.NoteHistories.Add(history);
await _context.SaveChangesAsync();
_logger.LogInformation("Note created: {NoteId} by {User}", note.Id, userEmail);
return note;
}
public async Task<Note> UpdateNoteAsync(Note note)
{
var existingNote = await _context.Notes
.FirstOrDefaultAsync(n => n.Id == note.Id);
if (existingNote == null)
{
throw new InvalidOperationException($"Note with ID {note.Id} not found.");
}
var userEmail = GetCurrentUserEmail();
var now = DateTime.UtcNow;
// Create history entry with previous state before updating
var history = new NoteHistory
{
NoteId = existingNote.Id,
Title = existingNote.Title,
Content = existingNote.Content,
ModifiedBy = userEmail,
ModifiedAt = now,
ChangeType = "Updated"
};
_context.NoteHistories.Add(history);
// Update the note
existingNote.Title = note.Title;
existingNote.Content = note.Content;
existingNote.UpdatedAt = now;
existingNote.LastModifiedBy = userEmail;
await _context.SaveChangesAsync();
_logger.LogInformation("Note updated: {NoteId} by {User}", note.Id, userEmail);
return existingNote;
}
public async Task DeleteNoteAsync(int id)
{
var note = await _context.Notes
.FirstOrDefaultAsync(n => n.Id == id);
if (note == null)
{
throw new InvalidOperationException($"Note with ID {id} not found.");
}
var userEmail = GetCurrentUserEmail();
var now = DateTime.UtcNow;
// Create deletion history entry
var history = new NoteHistory
{
NoteId = note.Id,
Title = note.Title,
Content = note.Content,
ModifiedBy = userEmail,
ModifiedAt = now,
ChangeType = "Soft Deleted"
};
_context.NoteHistories.Add(history);
// Soft delete - set IsDeleted flag instead of removing
note.IsDeleted = true;
note.UpdatedAt = now;
note.LastModifiedBy = userEmail;
await _context.SaveChangesAsync();
_logger.LogInformation("Note soft deleted: {NoteId} by {User}", id, userEmail);
}
public async Task<IEnumerable<Note>> GetPinnedNotesAsync()
{
return await _context.Notes
.AsNoTracking()
.Where(n => n.IsPinned && (n.Title == null || !n.Title.StartsWith("#")) && !n.IsDeleted)
.OrderByDescending(n => n.UpdatedAt)
.Take(3)
.ToListAsync();
}
public async Task TogglePinNoteAsync(int noteId)
{
var note = await _context.Notes
.FirstOrDefaultAsync(n => n.Id == noteId);
if (note == null)
{
throw new InvalidOperationException($"Note with ID {noteId} not found.");
}
// Prevent pinning page notes
if (_noteNamingService.IsPageNote(note.Title))
{
throw new InvalidOperationException("Page notes cannot be pinned.");
}
var userEmail = GetCurrentUserEmail();
var now = DateTime.UtcNow;
// If pinning and already 3 pinned notes exist (excluding this note if it's already pinned)
if (!note.IsPinned)
{
var pinnedCount = await _context.Notes
.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 == null || !n.Title.StartsWith("#")) && !n.IsDeleted)
.OrderBy(n => n.UpdatedAt)
.FirstOrDefaultAsync();
if (oldestPinned != null)
{
oldestPinned.IsPinned = false;
oldestPinned.UpdatedAt = now;
oldestPinned.LastModifiedBy = userEmail;
_logger.LogInformation("Auto-unpinned note {NoteId} (oldest) when pinning note {NewNoteId} by {User}",
oldestPinned.Id, noteId, userEmail);
}
}
}
// Toggle the pin status
note.IsPinned = !note.IsPinned;
note.UpdatedAt = now;
note.LastModifiedBy = userEmail;
await _context.SaveChangesAsync();
_logger.LogInformation("Note {NoteId} {Action} by {User}", noteId,
note.IsPinned ? "pinned" : "unpinned", userEmail);
}
public async Task<IEnumerable<Note>> GetDeletedNotesAsync()
{
return await _context.Notes
.AsNoTracking()
.Where(n => n.IsDeleted)
.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();
}
public async Task RestoreNoteAsync(int noteId)
{
var note = await _context.Notes
.FirstOrDefaultAsync(n => n.Id == noteId);
if (note == null)
{
throw new InvalidOperationException($"Note with ID {noteId} not found.");
}
if (!note.IsDeleted)
{
throw new InvalidOperationException($"Note with ID {noteId} is not deleted.");
}
var userEmail = GetCurrentUserEmail();
var now = DateTime.UtcNow;
// Create restore history entry
var history = new NoteHistory
{
NoteId = note.Id,
Title = note.Title,
Content = note.Content,
ModifiedBy = userEmail,
ModifiedAt = now,
ChangeType = "Restored"
};
_context.NoteHistories.Add(history);
// Restore the note
note.IsDeleted = false;
note.UpdatedAt = now;
note.LastModifiedBy = userEmail;
await _context.SaveChangesAsync();
_logger.LogInformation("Note restored: {NoteId} by {User}", noteId, userEmail);
}
}