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:
@@ -82,8 +82,9 @@ func (s *ResidenceService) ListResidences(userID uint) ([]responses.ResidenceRes
|
||||
}
|
||||
|
||||
// GetMyResidences returns residences with additional details (tasks, completions, etc.)
|
||||
// This is the "my-residences" endpoint that returns richer data
|
||||
func (s *ResidenceService) GetMyResidences(userID uint) (*responses.MyResidencesResponse, error) {
|
||||
// This is the "my-residences" endpoint that returns richer data.
|
||||
// The `now` parameter should be the start of day in the user's timezone for accurate overdue detection.
|
||||
func (s *ResidenceService) GetMyResidences(userID uint, now time.Time) (*responses.MyResidencesResponse, error) {
|
||||
residences, err := s.residenceRepo.FindByUser(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -104,8 +105,8 @@ func (s *ResidenceService) GetMyResidences(userID uint) (*responses.MyResidences
|
||||
residenceIDs[i] = r.ID
|
||||
}
|
||||
|
||||
// Get aggregated statistics
|
||||
stats, err := s.taskRepo.GetTaskStatistics(residenceIDs)
|
||||
// Get aggregated statistics using user's timezone-aware time
|
||||
stats, err := s.taskRepo.GetTaskStatistics(residenceIDs, now)
|
||||
if err == nil && stats != nil {
|
||||
summary.TotalTasks = stats.TotalTasks
|
||||
summary.TotalPending = stats.TotalPending
|
||||
@@ -114,8 +115,8 @@ func (s *ResidenceService) GetMyResidences(userID uint) (*responses.MyResidences
|
||||
summary.TasksDueNextMonth = stats.TasksDueNextMonth
|
||||
}
|
||||
|
||||
// Get per-residence overdue counts
|
||||
overdueCounts, err := s.taskRepo.GetOverdueCountByResidence(residenceIDs)
|
||||
// Get per-residence overdue counts using user's timezone-aware time
|
||||
overdueCounts, err := s.taskRepo.GetOverdueCountByResidence(residenceIDs, now)
|
||||
if err == nil && overdueCounts != nil {
|
||||
for i := range residenceResponses {
|
||||
if count, ok := overdueCounts[residenceResponses[i].ID]; ok {
|
||||
@@ -131,9 +132,10 @@ func (s *ResidenceService) GetMyResidences(userID uint) (*responses.MyResidences
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetSummary returns just the task summary statistics for a user's residences
|
||||
// This is a lightweight endpoint for refreshing summary counts without full residence data
|
||||
func (s *ResidenceService) GetSummary(userID uint) (*responses.TotalSummary, error) {
|
||||
// GetSummary returns just the task summary statistics for a user's residences.
|
||||
// This is a lightweight endpoint for refreshing summary counts without full residence data.
|
||||
// The `now` parameter should be the start of day in the user's timezone for accurate overdue detection.
|
||||
func (s *ResidenceService) GetSummary(userID uint, now time.Time) (*responses.TotalSummary, error) {
|
||||
residences, err := s.residenceRepo.FindByUser(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -151,8 +153,8 @@ func (s *ResidenceService) GetSummary(userID uint) (*responses.TotalSummary, err
|
||||
residenceIDs[i] = r.ID
|
||||
}
|
||||
|
||||
// Get aggregated statistics
|
||||
stats, err := s.taskRepo.GetTaskStatistics(residenceIDs)
|
||||
// Get aggregated statistics using user's timezone-aware time
|
||||
stats, err := s.taskRepo.GetTaskStatistics(residenceIDs, now)
|
||||
if err == nil && stats != nil {
|
||||
summary.TotalTasks = stats.TotalTasks
|
||||
summary.TotalPending = stats.TotalPending
|
||||
@@ -165,9 +167,10 @@ func (s *ResidenceService) GetSummary(userID uint) (*responses.TotalSummary, err
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
// getSummaryForUser is a helper that returns summary for a user, or empty summary on error
|
||||
// getSummaryForUser is a helper that returns summary for a user, or empty summary on error.
|
||||
// Uses UTC time. For timezone-aware summary, use GetSummary directly.
|
||||
func (s *ResidenceService) getSummaryForUser(userID uint) responses.TotalSummary {
|
||||
summary, err := s.GetSummary(userID)
|
||||
summary, err := s.GetSummary(userID, time.Now().UTC())
|
||||
if err != nil || summary == nil {
|
||||
return responses.TotalSummary{}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user