Fix task and shift self-assignment features
This commit is contained in:
@@ -42,20 +42,24 @@ public static class ShiftEndpoints
|
||||
|
||||
private static async Task<Ok<ShiftListDto>> GetShifts(
|
||||
ShiftService shiftService,
|
||||
HttpContext httpContext,
|
||||
[FromQuery] DateTimeOffset? from = null,
|
||||
[FromQuery] DateTimeOffset? to = null,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20)
|
||||
{
|
||||
var result = await shiftService.GetShiftsAsync(from, to, page, pageSize);
|
||||
var externalUserId = httpContext.User.FindFirst("sub")?.Value;
|
||||
var result = await shiftService.GetShiftsAsync(from, to, page, pageSize, externalUserId);
|
||||
return TypedResults.Ok(result);
|
||||
}
|
||||
|
||||
private static async Task<Results<Ok<ShiftDetailDto>, NotFound>> GetShift(
|
||||
Guid id,
|
||||
ShiftService shiftService)
|
||||
ShiftService shiftService,
|
||||
HttpContext httpContext)
|
||||
{
|
||||
var result = await shiftService.GetShiftByIdAsync(id);
|
||||
var externalUserId = httpContext.User.FindFirst("sub")?.Value;
|
||||
var result = await shiftService.GetShiftByIdAsync(id, externalUserId);
|
||||
|
||||
if (result == null)
|
||||
return TypedResults.NotFound();
|
||||
@@ -85,9 +89,11 @@ public static class ShiftEndpoints
|
||||
private static async Task<Results<Ok<ShiftDetailDto>, NotFound, Conflict<string>>> UpdateShift(
|
||||
Guid id,
|
||||
UpdateShiftRequest request,
|
||||
ShiftService shiftService)
|
||||
ShiftService shiftService,
|
||||
HttpContext httpContext)
|
||||
{
|
||||
var (shift, error, isConflict) = await shiftService.UpdateShiftAsync(id, request);
|
||||
var externalUserId = httpContext.User.FindFirst("sub")?.Value;
|
||||
var (shift, error, isConflict) = await shiftService.UpdateShiftAsync(id, request, externalUserId);
|
||||
|
||||
if (error != null)
|
||||
{
|
||||
|
||||
@@ -30,23 +30,35 @@ public static class TaskEndpoints
|
||||
group.MapDelete("{id:guid}", DeleteTask)
|
||||
.RequireAuthorization("RequireAdmin")
|
||||
.WithName("DeleteTask");
|
||||
|
||||
group.MapPost("{id:guid}/assign", AssignTaskToMe)
|
||||
.RequireAuthorization("RequireMember")
|
||||
.WithName("AssignTaskToMe");
|
||||
|
||||
group.MapDelete("{id:guid}/assign", UnassignTaskFromMe)
|
||||
.RequireAuthorization("RequireMember")
|
||||
.WithName("UnassignTaskFromMe");
|
||||
}
|
||||
|
||||
private static async Task<Ok<TaskListDto>> GetTasks(
|
||||
TaskService taskService,
|
||||
HttpContext httpContext,
|
||||
[FromQuery] string? status = null,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20)
|
||||
{
|
||||
var result = await taskService.GetTasksAsync(status, page, pageSize);
|
||||
var externalUserId = httpContext.User.FindFirst("sub")?.Value;
|
||||
var result = await taskService.GetTasksAsync(status, page, pageSize, externalUserId);
|
||||
return TypedResults.Ok(result);
|
||||
}
|
||||
|
||||
private static async Task<Results<Ok<TaskDetailDto>, NotFound>> GetTask(
|
||||
Guid id,
|
||||
TaskService taskService)
|
||||
TaskService taskService,
|
||||
HttpContext httpContext)
|
||||
{
|
||||
var result = await taskService.GetTaskByIdAsync(id);
|
||||
var externalUserId = httpContext.User.FindFirst("sub")?.Value;
|
||||
var result = await taskService.GetTaskByIdAsync(id, externalUserId);
|
||||
|
||||
if (result == null)
|
||||
return TypedResults.NotFound();
|
||||
@@ -76,9 +88,11 @@ public static class TaskEndpoints
|
||||
private static async Task<Results<Ok<TaskDetailDto>, NotFound, UnprocessableEntity<string>, Conflict<string>>> UpdateTask(
|
||||
Guid id,
|
||||
UpdateTaskRequest request,
|
||||
TaskService taskService)
|
||||
TaskService taskService,
|
||||
HttpContext httpContext)
|
||||
{
|
||||
var (task, error, isConflict) = await taskService.UpdateTaskAsync(id, request);
|
||||
var externalUserId = httpContext.User.FindFirst("sub")?.Value;
|
||||
var (task, error, isConflict) = await taskService.UpdateTaskAsync(id, request, externalUserId);
|
||||
|
||||
if (error != null)
|
||||
{
|
||||
@@ -105,4 +119,42 @@ public static class TaskEndpoints
|
||||
|
||||
return TypedResults.NoContent();
|
||||
}
|
||||
|
||||
private static async Task<Results<Ok, BadRequest<string>, NotFound>> AssignTaskToMe(
|
||||
Guid id,
|
||||
TaskService taskService,
|
||||
HttpContext httpContext)
|
||||
{
|
||||
var externalUserId = httpContext.User.FindFirst("sub")?.Value;
|
||||
if (externalUserId == null) return TypedResults.BadRequest("Invalid user");
|
||||
|
||||
var (success, error) = await taskService.AssignToMeAsync(id, externalUserId);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
if (error == "Task not found") return TypedResults.NotFound();
|
||||
return TypedResults.BadRequest(error ?? "Failed to assign task");
|
||||
}
|
||||
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
private static async Task<Results<Ok, BadRequest<string>, NotFound>> UnassignTaskFromMe(
|
||||
Guid id,
|
||||
TaskService taskService,
|
||||
HttpContext httpContext)
|
||||
{
|
||||
var externalUserId = httpContext.User.FindFirst("sub")?.Value;
|
||||
if (externalUserId == null) return TypedResults.BadRequest("Invalid user");
|
||||
|
||||
var (success, error) = await taskService.UnassignFromMeAsync(id, externalUserId);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
if (error == "Task not found") return TypedResults.NotFound();
|
||||
return TypedResults.BadRequest(error ?? "Failed to unassign task");
|
||||
}
|
||||
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ public class ShiftService
|
||||
_tenantProvider = tenantProvider;
|
||||
}
|
||||
|
||||
public async Task<ShiftListDto> GetShiftsAsync(DateTimeOffset? from, DateTimeOffset? to, int page, int pageSize)
|
||||
public async Task<ShiftListDto> GetShiftsAsync(DateTimeOffset? from, DateTimeOffset? to, int page, int pageSize, string? currentExternalUserId = null)
|
||||
{
|
||||
var query = _context.Shifts.AsQueryable();
|
||||
|
||||
@@ -42,19 +42,37 @@ public class ShiftService
|
||||
.Select(g => new { ShiftId = g.Key, Count = g.Count() })
|
||||
.ToDictionaryAsync(x => x.ShiftId, x => x.Count);
|
||||
|
||||
var tenantId = _tenantProvider.GetTenantId();
|
||||
var memberId = currentExternalUserId != null
|
||||
? await _context.Members
|
||||
.Where(m => m.ExternalUserId == currentExternalUserId && m.TenantId == tenantId)
|
||||
.Select(m => (Guid?)m.Id)
|
||||
.FirstOrDefaultAsync()
|
||||
: null;
|
||||
|
||||
var userSignups = memberId.HasValue
|
||||
? await _context.ShiftSignups
|
||||
.Where(ss => shiftIds.Contains(ss.ShiftId) && ss.MemberId == memberId.Value)
|
||||
.Select(ss => ss.ShiftId)
|
||||
.ToListAsync()
|
||||
: new List<Guid>();
|
||||
|
||||
var userSignedUpShiftIds = userSignups.ToHashSet();
|
||||
|
||||
var items = shifts.Select(s => new ShiftListItemDto(
|
||||
s.Id,
|
||||
s.Title,
|
||||
s.StartTime,
|
||||
s.EndTime,
|
||||
s.Capacity,
|
||||
signupCounts.GetValueOrDefault(s.Id, 0)
|
||||
signupCounts.GetValueOrDefault(s.Id, 0),
|
||||
userSignedUpShiftIds.Contains(s.Id)
|
||||
)).ToList();
|
||||
|
||||
return new ShiftListDto(items, total, page, pageSize);
|
||||
}
|
||||
|
||||
public async Task<ShiftDetailDto?> GetShiftByIdAsync(Guid id)
|
||||
public async Task<ShiftDetailDto?> GetShiftByIdAsync(Guid id, string? currentExternalUserId = null)
|
||||
{
|
||||
var shift = await _context.Shifts.FindAsync(id);
|
||||
|
||||
@@ -75,6 +93,8 @@ public class ShiftService
|
||||
ss.SignedUpAt
|
||||
)).ToList();
|
||||
|
||||
var isSignedUp = currentExternalUserId != null && signupDtos.Any(s => s.ExternalUserId == currentExternalUserId);
|
||||
|
||||
return new ShiftDetailDto(
|
||||
shift.Id,
|
||||
shift.Title,
|
||||
@@ -87,7 +107,8 @@ public class ShiftService
|
||||
shift.ClubId,
|
||||
shift.CreatedById,
|
||||
shift.CreatedAt,
|
||||
shift.UpdatedAt
|
||||
shift.UpdatedAt,
|
||||
isSignedUp
|
||||
);
|
||||
}
|
||||
|
||||
@@ -126,13 +147,14 @@ public class ShiftService
|
||||
shift.ClubId,
|
||||
shift.CreatedById,
|
||||
shift.CreatedAt,
|
||||
shift.UpdatedAt
|
||||
shift.UpdatedAt,
|
||||
false
|
||||
);
|
||||
|
||||
return (dto, null);
|
||||
}
|
||||
|
||||
public async Task<(ShiftDetailDto? shift, string? error, bool isConflict)> UpdateShiftAsync(Guid id, UpdateShiftRequest request)
|
||||
public async Task<(ShiftDetailDto? shift, string? error, bool isConflict)> UpdateShiftAsync(Guid id, UpdateShiftRequest request, string? currentExternalUserId = null)
|
||||
{
|
||||
var shift = await _context.Shifts.FindAsync(id);
|
||||
|
||||
@@ -182,6 +204,8 @@ public class ShiftService
|
||||
ss.SignedUpAt
|
||||
)).ToList();
|
||||
|
||||
var isSignedUp = currentExternalUserId != null && signupDtos.Any(s => s.ExternalUserId == currentExternalUserId);
|
||||
|
||||
var dto = new ShiftDetailDto(
|
||||
shift.Id,
|
||||
shift.Title,
|
||||
@@ -194,7 +218,8 @@ public class ShiftService
|
||||
shift.ClubId,
|
||||
shift.CreatedById,
|
||||
shift.CreatedAt,
|
||||
shift.UpdatedAt
|
||||
shift.UpdatedAt,
|
||||
isSignedUp
|
||||
);
|
||||
|
||||
return (dto, null, false);
|
||||
|
||||
@@ -18,7 +18,7 @@ public class TaskService
|
||||
_tenantProvider = tenantProvider;
|
||||
}
|
||||
|
||||
public async Task<TaskListDto> GetTasksAsync(string? statusFilter, int page, int pageSize)
|
||||
public async Task<TaskListDto> GetTasksAsync(string? statusFilter, int page, int pageSize, string? currentExternalUserId = null)
|
||||
{
|
||||
var query = _context.WorkItems.AsQueryable();
|
||||
|
||||
@@ -38,24 +38,45 @@ public class TaskService
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
Guid? memberId = null;
|
||||
if (currentExternalUserId != null)
|
||||
{
|
||||
var tenantId = _tenantProvider.GetTenantId();
|
||||
memberId = await _context.Members
|
||||
.Where(m => m.ExternalUserId == currentExternalUserId && m.TenantId == tenantId)
|
||||
.Select(m => m.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
var itemDtos = items.Select(w => new TaskListItemDto(
|
||||
w.Id,
|
||||
w.Title,
|
||||
w.Status.ToString(),
|
||||
w.AssigneeId,
|
||||
w.CreatedAt
|
||||
w.CreatedAt,
|
||||
memberId != null && w.AssigneeId == memberId
|
||||
)).ToList();
|
||||
|
||||
return new TaskListDto(itemDtos, total, page, pageSize);
|
||||
}
|
||||
|
||||
public async Task<TaskDetailDto?> GetTaskByIdAsync(Guid id)
|
||||
public async Task<TaskDetailDto?> GetTaskByIdAsync(Guid id, string? currentExternalUserId = null)
|
||||
{
|
||||
var workItem = await _context.WorkItems.FindAsync(id);
|
||||
|
||||
if (workItem == null)
|
||||
return null;
|
||||
|
||||
Guid? memberId = null;
|
||||
if (currentExternalUserId != null)
|
||||
{
|
||||
var tenantId = _tenantProvider.GetTenantId();
|
||||
memberId = await _context.Members
|
||||
.Where(m => m.ExternalUserId == currentExternalUserId && m.TenantId == tenantId)
|
||||
.Select(m => m.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
return new TaskDetailDto(
|
||||
workItem.Id,
|
||||
workItem.Title,
|
||||
@@ -66,7 +87,8 @@ public class TaskService
|
||||
workItem.ClubId,
|
||||
workItem.DueDate,
|
||||
workItem.CreatedAt,
|
||||
workItem.UpdatedAt
|
||||
workItem.UpdatedAt,
|
||||
memberId != null && workItem.AssigneeId == memberId
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,13 +124,14 @@ public class TaskService
|
||||
workItem.ClubId,
|
||||
workItem.DueDate,
|
||||
workItem.CreatedAt,
|
||||
workItem.UpdatedAt
|
||||
workItem.UpdatedAt,
|
||||
false
|
||||
);
|
||||
|
||||
return (dto, null);
|
||||
}
|
||||
|
||||
public async Task<(TaskDetailDto? task, string? error, bool isConflict)> UpdateTaskAsync(Guid id, UpdateTaskRequest request)
|
||||
public async Task<(TaskDetailDto? task, string? error, bool isConflict)> UpdateTaskAsync(Guid id, UpdateTaskRequest request, string? currentExternalUserId = null)
|
||||
{
|
||||
var workItem = await _context.WorkItems.FindAsync(id);
|
||||
|
||||
@@ -153,6 +176,16 @@ public class TaskService
|
||||
return (null, "Task was modified by another user. Please refresh and try again.", true);
|
||||
}
|
||||
|
||||
Guid? memberId = null;
|
||||
if (currentExternalUserId != null)
|
||||
{
|
||||
var tenantId = _tenantProvider.GetTenantId();
|
||||
memberId = await _context.Members
|
||||
.Where(m => m.ExternalUserId == currentExternalUserId && m.TenantId == tenantId)
|
||||
.Select(m => m.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
var dto = new TaskDetailDto(
|
||||
workItem.Id,
|
||||
workItem.Title,
|
||||
@@ -163,7 +196,8 @@ public class TaskService
|
||||
workItem.ClubId,
|
||||
workItem.DueDate,
|
||||
workItem.CreatedAt,
|
||||
workItem.UpdatedAt
|
||||
workItem.UpdatedAt,
|
||||
memberId != null && workItem.AssigneeId == memberId
|
||||
);
|
||||
|
||||
return (dto, null, false);
|
||||
@@ -181,4 +215,81 @@ public class TaskService
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<(bool success, string? error)> AssignToMeAsync(Guid taskId, string externalUserId)
|
||||
{
|
||||
var tenantId = _tenantProvider.GetTenantId();
|
||||
var memberId = await _context.Members
|
||||
.Where(m => m.ExternalUserId == externalUserId && m.TenantId == tenantId)
|
||||
.Select(m => m.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (memberId == Guid.Empty)
|
||||
return (false, "User is not a member of this club");
|
||||
|
||||
var workItem = await _context.WorkItems.FindAsync(taskId);
|
||||
if (workItem == null)
|
||||
return (false, "Task not found");
|
||||
|
||||
if (workItem.AssigneeId.HasValue)
|
||||
return (false, "Task is already assigned");
|
||||
|
||||
workItem.AssigneeId = memberId;
|
||||
|
||||
if (workItem.CanTransitionTo(WorkItemStatus.Assigned))
|
||||
workItem.TransitionTo(WorkItemStatus.Assigned);
|
||||
|
||||
workItem.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
try
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
return (false, "Task was modified by another user");
|
||||
}
|
||||
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
public async Task<(bool success, string? error)> UnassignFromMeAsync(Guid taskId, string externalUserId)
|
||||
{
|
||||
var tenantId = _tenantProvider.GetTenantId();
|
||||
var memberId = await _context.Members
|
||||
.Where(m => m.ExternalUserId == externalUserId && m.TenantId == tenantId)
|
||||
.Select(m => m.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (memberId == Guid.Empty)
|
||||
return (false, "User is not a member of this club");
|
||||
|
||||
var workItem = await _context.WorkItems.FindAsync(taskId);
|
||||
if (workItem == null)
|
||||
return (false, "Task not found");
|
||||
|
||||
if (workItem.AssigneeId != memberId)
|
||||
return (false, "Task is not assigned to you");
|
||||
|
||||
workItem.AssigneeId = null;
|
||||
|
||||
if (workItem.Status == WorkItemStatus.Assigned || workItem.Status == WorkItemStatus.InProgress)
|
||||
{
|
||||
// Transition back to open if no longer assigned and not marked Review/Done
|
||||
workItem.Status = WorkItemStatus.Open;
|
||||
}
|
||||
|
||||
workItem.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
try
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
return (false, "Task was modified by another user");
|
||||
}
|
||||
|
||||
return (true, null);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user