Add timezone-aware overdue task detection

Fix issue where tasks showed as "Overdue" on the server while displaying
"Tomorrow" on the client due to timezone differences between server (UTC)
and user's local timezone.

Changes:
- Add X-Timezone header support to extract user's timezone from requests
- Add TimezoneMiddleware to parse timezone and calculate user's local "today"
- Update task categorization to accept custom time for accurate date comparisons
- Update repository, service, and handler layers to pass timezone-aware time
- Update CORS to allow X-Timezone header

The client now sends the user's IANA timezone (e.g., "America/Los_Angeles")
and the server uses it to determine if a task is overdue based on the
user's local date, not UTC.

🤖 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-13 00:04:09 -06:00
parent a568658b58
commit 684856e0e9
8 changed files with 206 additions and 45 deletions

View File

@@ -55,12 +55,13 @@ func (s *TaskService) SetResidenceService(rs *ResidenceService) {
s.residenceService = rs
}
// getSummaryForUser gets the total summary for a user (helper for CRUD responses)
// getSummaryForUser gets the total summary for a user (helper for CRUD responses).
// Uses UTC time. For timezone-aware summary, call residence service directly.
func (s *TaskService) getSummaryForUser(userID uint) responses.TotalSummary {
if s.residenceService == nil {
return responses.TotalSummary{}
}
summary, err := s.residenceService.GetSummary(userID)
summary, err := s.residenceService.GetSummary(userID, time.Now().UTC())
if err != nil || summary == nil {
return responses.TotalSummary{}
}
@@ -92,8 +93,9 @@ func (s *TaskService) GetTask(taskID, userID uint) (*responses.TaskResponse, err
return &resp, nil
}
// ListTasks lists all tasks accessible to a user as a kanban board
func (s *TaskService) ListTasks(userID uint) (*responses.KanbanBoardResponse, error) {
// ListTasks lists all tasks accessible to a user as a kanban board.
// The `now` parameter should be the start of day in the user's timezone for accurate overdue detection.
func (s *TaskService) ListTasks(userID uint, now time.Time) (*responses.KanbanBoardResponse, error) {
// Get all residence IDs accessible to user
residences, err := s.residenceRepo.FindByUser(userID)
if err != nil {
@@ -114,8 +116,8 @@ func (s *TaskService) ListTasks(userID uint) (*responses.KanbanBoardResponse, er
}, nil
}
// Get kanban data aggregated across all residences
board, err := s.taskRepo.GetKanbanDataForMultipleResidences(residenceIDs, 30)
// Get kanban data aggregated across all residences using user's timezone-aware time
board, err := s.taskRepo.GetKanbanDataForMultipleResidences(residenceIDs, 30, now)
if err != nil {
return nil, err
}
@@ -127,8 +129,9 @@ func (s *TaskService) ListTasks(userID uint) (*responses.KanbanBoardResponse, er
return &resp, nil
}
// GetTasksByResidence gets tasks for a specific residence (kanban board)
func (s *TaskService) GetTasksByResidence(residenceID, userID uint, daysThreshold int) (*responses.KanbanBoardResponse, error) {
// GetTasksByResidence gets tasks for a specific residence (kanban board).
// The `now` parameter should be the start of day in the user's timezone for accurate overdue detection.
func (s *TaskService) GetTasksByResidence(residenceID, userID uint, daysThreshold int, now time.Time) (*responses.KanbanBoardResponse, error) {
// Check access
hasAccess, err := s.residenceRepo.HasAccess(residenceID, userID)
if err != nil {
@@ -142,7 +145,8 @@ func (s *TaskService) GetTasksByResidence(residenceID, userID uint, daysThreshol
daysThreshold = 30 // Default
}
board, err := s.taskRepo.GetKanbanData(residenceID, daysThreshold)
// Get kanban data using user's timezone-aware time
board, err := s.taskRepo.GetKanbanData(residenceID, daysThreshold, now)
if err != nil {
return nil, err
}