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:
@@ -40,7 +40,8 @@ type Context struct {
|
||||
DaysThreshold int
|
||||
}
|
||||
|
||||
// NewContext creates a new categorization context with sensible defaults
|
||||
// NewContext creates a new categorization context with sensible defaults.
|
||||
// Uses UTC time. For timezone-aware categorization, use NewContextWithTime.
|
||||
func NewContext(t *models.Task, daysThreshold int) *Context {
|
||||
if daysThreshold <= 0 {
|
||||
daysThreshold = 30
|
||||
@@ -52,6 +53,20 @@ func NewContext(t *models.Task, daysThreshold int) *Context {
|
||||
}
|
||||
}
|
||||
|
||||
// NewContextWithTime creates a new categorization context with a specific time.
|
||||
// Use this when you need timezone-aware categorization - pass the start of day
|
||||
// in the user's timezone.
|
||||
func NewContextWithTime(t *models.Task, daysThreshold int, now time.Time) *Context {
|
||||
if daysThreshold <= 0 {
|
||||
daysThreshold = 30
|
||||
}
|
||||
return &Context{
|
||||
Task: t,
|
||||
Now: now,
|
||||
DaysThreshold: daysThreshold,
|
||||
}
|
||||
}
|
||||
|
||||
// ThresholdDate returns the date threshold for "due soon" categorization
|
||||
func (c *Context) ThresholdDate() time.Time {
|
||||
return c.Now.AddDate(0, 0, c.DaysThreshold)
|
||||
@@ -207,12 +222,21 @@ func NewChain() *Chain {
|
||||
return &Chain{head: cancelled}
|
||||
}
|
||||
|
||||
// Categorize determines which kanban column a task belongs to
|
||||
// Categorize determines which kanban column a task belongs to.
|
||||
// Uses UTC time. For timezone-aware categorization, use CategorizeWithTime.
|
||||
func (c *Chain) Categorize(t *models.Task, daysThreshold int) KanbanColumn {
|
||||
ctx := NewContext(t, daysThreshold)
|
||||
return c.head.Handle(ctx)
|
||||
}
|
||||
|
||||
// CategorizeWithTime determines which kanban column a task belongs to using a specific time.
|
||||
// Use this when you need timezone-aware categorization - pass the start of day
|
||||
// in the user's timezone.
|
||||
func (c *Chain) CategorizeWithTime(t *models.Task, daysThreshold int, now time.Time) KanbanColumn {
|
||||
ctx := NewContextWithTime(t, daysThreshold, now)
|
||||
return c.head.Handle(ctx)
|
||||
}
|
||||
|
||||
// CategorizeWithContext uses a pre-built context for categorization
|
||||
func (c *Chain) CategorizeWithContext(ctx *Context) KanbanColumn {
|
||||
return c.head.Handle(ctx)
|
||||
@@ -223,18 +247,41 @@ func (c *Chain) CategorizeWithContext(ctx *Context) KanbanColumn {
|
||||
// defaultChain is a singleton chain instance for convenience
|
||||
var defaultChain = NewChain()
|
||||
|
||||
// DetermineKanbanColumn is a convenience function that uses the default chain
|
||||
// DetermineKanbanColumn is a convenience function that uses the default chain.
|
||||
// Uses UTC time. For timezone-aware categorization, use DetermineKanbanColumnWithTime.
|
||||
func DetermineKanbanColumn(t *models.Task, daysThreshold int) string {
|
||||
return defaultChain.Categorize(t, daysThreshold).String()
|
||||
}
|
||||
|
||||
// CategorizeTask is an alias for DetermineKanbanColumn with a more descriptive name
|
||||
// DetermineKanbanColumnWithTime is a convenience function that uses a specific time.
|
||||
// Use this when you need timezone-aware categorization.
|
||||
func DetermineKanbanColumnWithTime(t *models.Task, daysThreshold int, now time.Time) string {
|
||||
return defaultChain.CategorizeWithTime(t, daysThreshold, now).String()
|
||||
}
|
||||
|
||||
// CategorizeTask is an alias for DetermineKanbanColumn with a more descriptive name.
|
||||
// Uses UTC time. For timezone-aware categorization, use CategorizeTaskWithTime.
|
||||
func CategorizeTask(t *models.Task, daysThreshold int) KanbanColumn {
|
||||
return defaultChain.Categorize(t, daysThreshold)
|
||||
}
|
||||
|
||||
// CategorizeTasksIntoColumns categorizes multiple tasks into their respective columns
|
||||
// CategorizeTaskWithTime categorizes a task using a specific time.
|
||||
// Use this when you need timezone-aware categorization - pass the start of day
|
||||
// in the user's timezone.
|
||||
func CategorizeTaskWithTime(t *models.Task, daysThreshold int, now time.Time) KanbanColumn {
|
||||
return defaultChain.CategorizeWithTime(t, daysThreshold, now)
|
||||
}
|
||||
|
||||
// CategorizeTasksIntoColumns categorizes multiple tasks into their respective columns.
|
||||
// Uses UTC time. For timezone-aware categorization, use CategorizeTasksIntoColumnsWithTime.
|
||||
func CategorizeTasksIntoColumns(tasks []models.Task, daysThreshold int) map[KanbanColumn][]models.Task {
|
||||
return CategorizeTasksIntoColumnsWithTime(tasks, daysThreshold, time.Now().UTC())
|
||||
}
|
||||
|
||||
// CategorizeTasksIntoColumnsWithTime categorizes multiple tasks using a specific time.
|
||||
// Use this when you need timezone-aware categorization - pass the start of day
|
||||
// in the user's timezone.
|
||||
func CategorizeTasksIntoColumnsWithTime(tasks []models.Task, daysThreshold int, now time.Time) map[KanbanColumn][]models.Task {
|
||||
result := make(map[KanbanColumn][]models.Task)
|
||||
|
||||
// Initialize all columns with empty slices
|
||||
@@ -248,7 +295,7 @@ func CategorizeTasksIntoColumns(tasks []models.Task, daysThreshold int) map[Kanb
|
||||
// Categorize each task
|
||||
chain := NewChain()
|
||||
for _, t := range tasks {
|
||||
column := chain.Categorize(&t, daysThreshold)
|
||||
column := chain.CategorizeWithTime(&t, daysThreshold, now)
|
||||
result[column] = append(result[column], t)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user