Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions backend/src/Taskdeck.Application/Services/BoardService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Taskdeck.Application.DTOs;
using Taskdeck.Application.DTOs;
using Taskdeck.Application.Interfaces;
using Taskdeck.Domain.Common;
using Taskdeck.Domain.Entities;
Expand Down Expand Up @@ -168,6 +168,11 @@ private async Task<Result<BoardDto>> UpdateBoardInternalAsync(Guid id, UpdateBoa
if (board == null)
return Result.Failure<BoardDto>(ErrorCodes.NotFound, $"Board with ID {id} not found");

// Capture pre-mutation state for change summary
var oldName = board.Name;
var oldDescription = board.Description;
var oldIsArchived = board.IsArchived;

if (dto.Name != null || dto.Description != null)
board.Update(dto.Name, dto.Description);

Expand All @@ -183,12 +188,15 @@ private async Task<Result<BoardDto>> UpdateBoardInternalAsync(Guid id, UpdateBoa
await _realtimeNotifier.NotifyBoardMutationAsync(
new BoardRealtimeEvent(board.Id, "board", "updated", board.Id, DateTimeOffset.UtcNow),
cancellationToken);

var changeSummary = BuildBoardChangeSummary(dto, oldName, oldDescription, oldIsArchived);

if (dto.IsArchived == true)
await SafeLogAsync("board", board.Id, AuditAction.Archived, changes: $"name={board.Name}");
await SafeLogAsync("board", board.Id, AuditAction.Archived, changes: changeSummary);
else if (dto.IsArchived == false)
await SafeLogAsync("board", board.Id, AuditAction.Updated, changes: $"unarchived; name={board.Name}");
await SafeLogAsync("board", board.Id, AuditAction.Unarchived, changes: changeSummary);
else
await SafeLogAsync("board", board.Id, AuditAction.Updated, changes: $"name={board.Name}");
await SafeLogAsync("board", board.Id, AuditAction.Updated, changes: changeSummary);
return Result.Success(MapToDto(board));
}
catch (DomainException ex)
Expand All @@ -197,6 +205,18 @@ await _realtimeNotifier.NotifyBoardMutationAsync(
}
}

private static string BuildBoardChangeSummary(UpdateBoardDto dto, string oldName, string? oldDescription, bool oldIsArchived)
{
var parts = new List<string>();
if (dto.Name != null && dto.Name != oldName)
parts.Add($"Name: '{oldName}' -> '{dto.Name}'");
if (dto.Description != null && dto.Description != oldDescription)
parts.Add($"Description changed");
if (dto.IsArchived.HasValue && dto.IsArchived.Value != oldIsArchived)
parts.Add(dto.IsArchived.Value ? "Archived" : "Unarchived");
return parts.Count > 0 ? string.Join("; ", parts) : "no fields changed";
}

public async Task<Result<BoardDto>> GetBoardByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
var board = await _unitOfWork.Boards.GetByIdAsync(id, cancellationToken);
Expand Down
46 changes: 44 additions & 2 deletions backend/src/Taskdeck.Application/Services/CardService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Taskdeck.Application.DTOs;
using Taskdeck.Application.DTOs;
using Taskdeck.Application.Interfaces;
using Taskdeck.Domain.Common;
using Taskdeck.Domain.Entities;
Expand Down Expand Up @@ -113,6 +113,14 @@ public async Task<Result<CardDto>> UpdateCardAsync(
"Card was updated by another session. Refresh and retry your changes.");
}

// Capture pre-mutation state for change summary
var oldTitle = card.Title;
var oldDescription = card.Description;
var oldDueDate = card.DueDate;
var oldIsBlocked = card.IsBlocked;
var oldBlockReason = card.BlockReason;
var oldLabelIds = card.CardLabels.Select(cl => cl.LabelId).OrderBy(id => id).ToList();

// Update basic fields
if (dto.Title != null || dto.Description != null || dto.DueDate.HasValue)
card.Update(dto.Title, dto.Description, dto.DueDate);
Expand Down Expand Up @@ -140,11 +148,13 @@ public async Task<Result<CardDto>> UpdateCardAsync(
}
}

var changeSummary = BuildCardChangeSummary(dto, oldTitle, oldDescription, oldDueDate, oldIsBlocked, oldBlockReason, oldLabelIds);

await _unitOfWork.SaveChangesAsync(cancellationToken);
await _realtimeNotifier.NotifyBoardMutationAsync(
new BoardRealtimeEvent(card.BoardId, "card", "updated", card.Id, DateTimeOffset.UtcNow),
cancellationToken);
await SafeLogAsync("card", card.Id, AuditAction.Updated, actorUserId);
await SafeLogAsync("card", card.Id, AuditAction.Updated, actorUserId, changeSummary);

var updatedCard = await _unitOfWork.Cards.GetByIdWithLabelsAsync(id, cancellationToken);
return Result.Success(MapToDto(updatedCard!));
Expand All @@ -155,6 +165,38 @@ await _realtimeNotifier.NotifyBoardMutationAsync(
}
}

private static string BuildCardChangeSummary(
UpdateCardDto dto,
string oldTitle,
string? oldDescription,
DateTimeOffset? oldDueDate,
bool oldIsBlocked,
string? oldBlockReason,
List<Guid> oldLabelIds)
{
var parts = new List<string>();
if (dto.Title != null && dto.Title != oldTitle)
parts.Add($"Title: '{oldTitle}' -> '{dto.Title}'");
if (dto.Description != null && dto.Description != oldDescription)
parts.Add("Description changed");
if (dto.DueDate.HasValue && dto.DueDate.Value != oldDueDate)
parts.Add($"DueDate: '{oldDueDate?.ToString("O") ?? "none"}' -> '{dto.DueDate.Value:O}'");
if (dto.IsBlocked.HasValue && dto.IsBlocked.Value != oldIsBlocked)
{
if (dto.IsBlocked.Value && !string.IsNullOrEmpty(dto.BlockReason) && !oldIsBlocked)
parts.Add($"Blocked: {dto.BlockReason}");
else if (!dto.IsBlocked.Value && oldIsBlocked)
parts.Add("Unblocked");
}
if (dto.LabelIds != null)
{
var newLabelIds = dto.LabelIds.OrderBy(id => id).ToList();
if (!oldLabelIds.SequenceEqual(newLabelIds))
parts.Add($"Labels changed: {oldLabelIds.Count} -> {newLabelIds.Count}");
}
return parts.Count > 0 ? string.Join("; ", parts) : "no fields changed";
}

public Task<Result<CardDto>> UpdateCardAsync(
Guid id,
UpdateCardDto dto,
Expand Down
23 changes: 21 additions & 2 deletions backend/src/Taskdeck.Application/Services/ColumnService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Taskdeck.Application.DTOs;
using Taskdeck.Application.DTOs;
using Taskdeck.Application.Interfaces;
using Taskdeck.Domain.Common;
using Taskdeck.Domain.Entities;
Expand Down Expand Up @@ -73,12 +73,19 @@ public async Task<Result<ColumnDto>> UpdateColumnAsync(Guid id, UpdateColumnDto
if (column == null)
return Result.Failure<ColumnDto>(ErrorCodes.NotFound, $"Column with ID {id} not found");

// Capture pre-mutation state for change summary
var oldName = column.Name;
var oldWipLimit = column.WipLimit;
var oldPosition = column.Position;

column.Update(dto.Name, dto.WipLimit, dto.Position);
await _unitOfWork.SaveChangesAsync(cancellationToken);
await _realtimeNotifier.NotifyBoardMutationAsync(
new BoardRealtimeEvent(column.BoardId, "column", "updated", column.Id, DateTimeOffset.UtcNow),
cancellationToken);
await SafeLogAsync("column", column.Id, AuditAction.Updated);

var changeSummary = BuildColumnChangeSummary(dto, oldName, oldWipLimit, oldPosition);
await SafeLogAsync("column", column.Id, AuditAction.Updated, changes: changeSummary);

return Result.Success(MapToDto(column));
}
Expand All @@ -88,6 +95,18 @@ await _realtimeNotifier.NotifyBoardMutationAsync(
}
}

private static string BuildColumnChangeSummary(UpdateColumnDto dto, string oldName, int? oldWipLimit, int oldPosition)
{
var parts = new List<string>();
if (dto.Name != null && dto.Name != oldName)
parts.Add($"Name: '{oldName}' -> '{dto.Name}'");
if (dto.WipLimit.HasValue && dto.WipLimit.Value != oldWipLimit)
parts.Add($"WipLimit: {oldWipLimit?.ToString() ?? "none"} -> {dto.WipLimit.Value}");
if (dto.Position.HasValue && dto.Position.Value != oldPosition)
parts.Add($"Position: {oldPosition} -> {dto.Position.Value}");
return parts.Count > 0 ? string.Join("; ", parts) : "no fields changed";
}

public async Task<Result<ColumnDto>> UpdateColumnAsync(Guid boardId, Guid id, UpdateColumnDto dto, CancellationToken cancellationToken = default)
{
var column = await _unitOfWork.Columns.GetByIdAsync(id, cancellationToken);
Expand Down
20 changes: 18 additions & 2 deletions backend/src/Taskdeck.Application/Services/LabelService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Taskdeck.Application.DTOs;
using Taskdeck.Application.DTOs;
using Taskdeck.Application.Interfaces;
using Taskdeck.Domain.Common;
using Taskdeck.Domain.Entities;
Expand Down Expand Up @@ -64,12 +64,18 @@ public async Task<Result<LabelDto>> UpdateLabelAsync(Guid id, UpdateLabelDto dto
if (label == null)
return Result.Failure<LabelDto>(ErrorCodes.NotFound, $"Label with ID {id} not found");

// Capture pre-mutation state for change summary
var oldName = label.Name;
var oldColorHex = label.ColorHex;

label.Update(dto.Name, dto.ColorHex);
await _unitOfWork.SaveChangesAsync(cancellationToken);
await _realtimeNotifier.NotifyBoardMutationAsync(
new BoardRealtimeEvent(label.BoardId, "label", "updated", label.Id, DateTimeOffset.UtcNow),
cancellationToken);
await SafeLogAsync("label", label.Id, AuditAction.Updated);

var changeSummary = BuildLabelChangeSummary(dto, oldName, oldColorHex);
await SafeLogAsync("label", label.Id, AuditAction.Updated, changes: changeSummary);

return Result.Success(MapToDto(label));
}
Expand All @@ -79,6 +85,16 @@ await _realtimeNotifier.NotifyBoardMutationAsync(
}
}

private static string BuildLabelChangeSummary(UpdateLabelDto dto, string oldName, string oldColorHex)
{
var parts = new List<string>();
if (dto.Name != null && dto.Name != oldName)
parts.Add($"Name: '{oldName}' -> '{dto.Name}'");
if (dto.ColorHex != null && dto.ColorHex != oldColorHex)
parts.Add($"Color: '{oldColorHex}' -> '{dto.ColorHex}'");
return parts.Count > 0 ? string.Join("; ", parts) : "no fields changed";
}

public async Task<Result<LabelDto>> UpdateLabelAsync(Guid boardId, Guid id, UpdateLabelDto dto, CancellationToken cancellationToken = default)
{
var label = await _unitOfWork.Labels.GetByIdAsync(id, cancellationToken);
Expand Down
Loading
Loading