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:
@@ -131,9 +131,10 @@ func (r *TaskRepository) Unarchive(id uint) error {
|
||||
|
||||
// === Kanban Board ===
|
||||
|
||||
// GetKanbanData retrieves tasks organized for kanban display
|
||||
// GetKanbanData retrieves tasks organized for kanban display.
|
||||
// Uses the task.categorization package as the single source of truth for categorization logic.
|
||||
func (r *TaskRepository) GetKanbanData(residenceID uint, daysThreshold int) (*models.KanbanBoard, error) {
|
||||
// The `now` parameter should be the start of day in the user's timezone for accurate overdue detection.
|
||||
func (r *TaskRepository) GetKanbanData(residenceID uint, daysThreshold int, now time.Time) (*models.KanbanBoard, error) {
|
||||
var tasks []models.Task
|
||||
err := r.db.Preload("CreatedBy").
|
||||
Preload("AssignedTo").
|
||||
@@ -151,7 +152,8 @@ func (r *TaskRepository) GetKanbanData(residenceID uint, daysThreshold int) (*mo
|
||||
}
|
||||
|
||||
// Use the categorization package as the single source of truth
|
||||
categorized := categorization.CategorizeTasksIntoColumns(tasks, daysThreshold)
|
||||
// Pass the user's timezone-aware time for accurate overdue detection
|
||||
categorized := categorization.CategorizeTasksIntoColumnsWithTime(tasks, daysThreshold, now)
|
||||
|
||||
columns := []models.KanbanColumn{
|
||||
{
|
||||
@@ -217,9 +219,10 @@ func (r *TaskRepository) GetKanbanData(residenceID uint, daysThreshold int) (*mo
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetKanbanDataForMultipleResidences retrieves tasks from multiple residences organized for kanban display
|
||||
// GetKanbanDataForMultipleResidences retrieves tasks from multiple residences organized for kanban display.
|
||||
// Uses the task.categorization package as the single source of truth for categorization logic.
|
||||
func (r *TaskRepository) GetKanbanDataForMultipleResidences(residenceIDs []uint, daysThreshold int) (*models.KanbanBoard, error) {
|
||||
// The `now` parameter should be the start of day in the user's timezone for accurate overdue detection.
|
||||
func (r *TaskRepository) GetKanbanDataForMultipleResidences(residenceIDs []uint, daysThreshold int, now time.Time) (*models.KanbanBoard, error) {
|
||||
var tasks []models.Task
|
||||
err := r.db.Preload("CreatedBy").
|
||||
Preload("AssignedTo").
|
||||
@@ -238,7 +241,8 @@ func (r *TaskRepository) GetKanbanDataForMultipleResidences(residenceIDs []uint,
|
||||
}
|
||||
|
||||
// Use the categorization package as the single source of truth
|
||||
categorized := categorization.CategorizeTasksIntoColumns(tasks, daysThreshold)
|
||||
// Pass the user's timezone-aware time for accurate overdue detection
|
||||
categorized := categorization.CategorizeTasksIntoColumnsWithTime(tasks, daysThreshold, now)
|
||||
|
||||
columns := []models.KanbanColumn{
|
||||
{
|
||||
@@ -418,13 +422,12 @@ type TaskStatistics struct {
|
||||
|
||||
// GetTaskStatistics returns aggregated task statistics for multiple residences.
|
||||
// Uses the task.scopes package for consistent filtering logic.
|
||||
func (r *TaskRepository) GetTaskStatistics(residenceIDs []uint) (*TaskStatistics, error) {
|
||||
// The `now` parameter should be the start of day in the user's timezone for accurate overdue detection.
|
||||
func (r *TaskRepository) GetTaskStatistics(residenceIDs []uint, now time.Time) (*TaskStatistics, error) {
|
||||
if len(residenceIDs) == 0 {
|
||||
return &TaskStatistics{}, nil
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
var totalTasks, totalOverdue, totalPending, tasksDueNextWeek, tasksDueNextMonth int64
|
||||
|
||||
// Count total active tasks (not cancelled, not archived)
|
||||
@@ -483,13 +486,12 @@ func (r *TaskRepository) GetTaskStatistics(residenceIDs []uint) (*TaskStatistics
|
||||
|
||||
// GetOverdueCountByResidence returns a map of residence ID to overdue task count.
|
||||
// Uses the task.scopes package for consistent filtering logic.
|
||||
func (r *TaskRepository) GetOverdueCountByResidence(residenceIDs []uint) (map[uint]int, error) {
|
||||
// The `now` parameter should be the start of day in the user's timezone for accurate overdue detection.
|
||||
func (r *TaskRepository) GetOverdueCountByResidence(residenceIDs []uint, now time.Time) (map[uint]int, error) {
|
||||
if len(residenceIDs) == 0 {
|
||||
return map[uint]int{}, nil
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
// Query to get overdue count grouped by residence
|
||||
type result struct {
|
||||
ResidenceID uint
|
||||
|
||||
Reference in New Issue
Block a user