Fix timezone bug in task kanban categorization

Task creation/update responses were using UTC time for kanban column
categorization, causing tasks to incorrectly appear as overdue when
the server had passed midnight UTC but the user's local time was still
the previous day.

Changes:
- Add timezone-aware response functions (NewTaskResponseWithTime, etc.)
- Pass userNow from middleware to all task service methods
- Update handlers to use timezone-aware time from X-Timezone header
- Update tests to pass the now parameter

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-15 20:56:02 -06:00
parent e34d222634
commit c51f1ce34a
4 changed files with 118 additions and 52 deletions

View File

@@ -149,8 +149,9 @@ func (s *TaskService) GetTasksByResidence(residenceID, userID uint, daysThreshol
return &resp, nil
}
// CreateTask creates a new task
func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint) (*responses.TaskWithSummaryResponse, error) {
// CreateTask creates a new task.
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization.
func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
// Check residence access
hasAccess, err := s.residenceRepo.HasAccess(req.ResidenceID, userID)
if err != nil {
@@ -189,13 +190,14 @@ func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint) (
}
return &responses.TaskWithSummaryResponse{
Data: responses.NewTaskResponse(task),
Data: responses.NewTaskResponseWithTime(task, 30, now),
Summary: s.getSummaryForUser(userID),
}, nil
}
// UpdateTask updates a task
func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRequest) (*responses.TaskWithSummaryResponse, error) {
// UpdateTask updates a task.
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization.
func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRequest, now time.Time) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -268,7 +270,7 @@ func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRe
}
return &responses.TaskWithSummaryResponse{
Data: responses.NewTaskResponse(task),
Data: responses.NewTaskResponseWithTime(task, 30, now),
Summary: s.getSummaryForUser(userID),
}, nil
}
@@ -304,8 +306,9 @@ func (s *TaskService) DeleteTask(taskID, userID uint) (*responses.DeleteWithSumm
// === Task Actions ===
// MarkInProgress marks a task as in progress
func (s *TaskService) MarkInProgress(taskID, userID uint) (*responses.TaskWithSummaryResponse, error) {
// MarkInProgress marks a task as in progress.
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization.
func (s *TaskService) MarkInProgress(taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -334,13 +337,14 @@ func (s *TaskService) MarkInProgress(taskID, userID uint) (*responses.TaskWithSu
}
return &responses.TaskWithSummaryResponse{
Data: responses.NewTaskResponse(task),
Data: responses.NewTaskResponseWithTime(task, 30, now),
Summary: s.getSummaryForUser(userID),
}, nil
}
// CancelTask cancels a task
func (s *TaskService) CancelTask(taskID, userID uint) (*responses.TaskWithSummaryResponse, error) {
// CancelTask cancels a task.
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization.
func (s *TaskService) CancelTask(taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -373,13 +377,14 @@ func (s *TaskService) CancelTask(taskID, userID uint) (*responses.TaskWithSummar
}
return &responses.TaskWithSummaryResponse{
Data: responses.NewTaskResponse(task),
Data: responses.NewTaskResponseWithTime(task, 30, now),
Summary: s.getSummaryForUser(userID),
}, nil
}
// UncancelTask uncancels a task
func (s *TaskService) UncancelTask(taskID, userID uint) (*responses.TaskWithSummaryResponse, error) {
// UncancelTask uncancels a task.
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization.
func (s *TaskService) UncancelTask(taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -408,13 +413,14 @@ func (s *TaskService) UncancelTask(taskID, userID uint) (*responses.TaskWithSumm
}
return &responses.TaskWithSummaryResponse{
Data: responses.NewTaskResponse(task),
Data: responses.NewTaskResponseWithTime(task, 30, now),
Summary: s.getSummaryForUser(userID),
}, nil
}
// ArchiveTask archives a task
func (s *TaskService) ArchiveTask(taskID, userID uint) (*responses.TaskWithSummaryResponse, error) {
// ArchiveTask archives a task.
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization.
func (s *TaskService) ArchiveTask(taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -447,13 +453,14 @@ func (s *TaskService) ArchiveTask(taskID, userID uint) (*responses.TaskWithSumma
}
return &responses.TaskWithSummaryResponse{
Data: responses.NewTaskResponse(task),
Data: responses.NewTaskResponseWithTime(task, 30, now),
Summary: s.getSummaryForUser(userID),
}, nil
}
// UnarchiveTask unarchives a task
func (s *TaskService) UnarchiveTask(taskID, userID uint) (*responses.TaskWithSummaryResponse, error) {
// UnarchiveTask unarchives a task.
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization.
func (s *TaskService) UnarchiveTask(taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -482,15 +489,16 @@ func (s *TaskService) UnarchiveTask(taskID, userID uint) (*responses.TaskWithSum
}
return &responses.TaskWithSummaryResponse{
Data: responses.NewTaskResponse(task),
Data: responses.NewTaskResponseWithTime(task, 30, now),
Summary: s.getSummaryForUser(userID),
}, nil
}
// === Task Completions ===
// CreateCompletion creates a task completion
func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest, userID uint) (*responses.TaskCompletionWithSummaryResponse, error) {
// CreateCompletion creates a task completion.
// The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization.
func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest, userID uint, now time.Time) (*responses.TaskCompletionWithSummaryResponse, error) {
// Get the task
task, err := s.taskRepo.FindByID(req.TaskID)
if err != nil {
@@ -600,7 +608,7 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest
s.sendTaskCompletedNotification(task, completion)
// Return completion with updated task (includes kanban_column for UI update)
resp := responses.NewTaskCompletionWithTaskResponse(completion, task, 30)
resp := responses.NewTaskCompletionWithTaskResponseWithTime(completion, task, 30, now)
return &responses.TaskCompletionWithSummaryResponse{
Data: resp,
Summary: s.getSummaryForUser(userID),