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

@@ -103,13 +103,15 @@ func (h *TaskHandler) GetTasksByResidence(c *gin.Context) {
// CreateTask handles POST /api/tasks/
func (h *TaskHandler) CreateTask(c *gin.Context) {
user := c.MustGet(middleware.AuthUserKey).(*models.User)
userNow := middleware.GetUserNow(c)
var req requests.CreateTaskRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
response, err := h.taskService.CreateTask(&req, user.ID)
response, err := h.taskService.CreateTask(&req, user.ID, userNow)
if err != nil {
if errors.Is(err, services.ErrResidenceAccessDenied) {
c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_access_denied")})
@@ -124,6 +126,8 @@ func (h *TaskHandler) CreateTask(c *gin.Context) {
// UpdateTask handles PUT/PATCH /api/tasks/:id/
func (h *TaskHandler) UpdateTask(c *gin.Context) {
user := c.MustGet(middleware.AuthUserKey).(*models.User)
userNow := middleware.GetUserNow(c)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
@@ -136,7 +140,7 @@ func (h *TaskHandler) UpdateTask(c *gin.Context) {
return
}
response, err := h.taskService.UpdateTask(uint(taskID), user.ID, &req)
response, err := h.taskService.UpdateTask(uint(taskID), user.ID, &req, userNow)
if err != nil {
switch {
case errors.Is(err, services.ErrTaskNotFound):
@@ -178,13 +182,15 @@ func (h *TaskHandler) DeleteTask(c *gin.Context) {
// MarkInProgress handles POST /api/tasks/:id/mark-in-progress/
func (h *TaskHandler) MarkInProgress(c *gin.Context) {
user := c.MustGet(middleware.AuthUserKey).(*models.User)
userNow := middleware.GetUserNow(c)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
return
}
response, err := h.taskService.MarkInProgress(uint(taskID), user.ID)
response, err := h.taskService.MarkInProgress(uint(taskID), user.ID, userNow)
if err != nil {
switch {
case errors.Is(err, services.ErrTaskNotFound):
@@ -202,13 +208,15 @@ func (h *TaskHandler) MarkInProgress(c *gin.Context) {
// CancelTask handles POST /api/tasks/:id/cancel/
func (h *TaskHandler) CancelTask(c *gin.Context) {
user := c.MustGet(middleware.AuthUserKey).(*models.User)
userNow := middleware.GetUserNow(c)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
return
}
response, err := h.taskService.CancelTask(uint(taskID), user.ID)
response, err := h.taskService.CancelTask(uint(taskID), user.ID, userNow)
if err != nil {
switch {
case errors.Is(err, services.ErrTaskNotFound):
@@ -228,13 +236,15 @@ func (h *TaskHandler) CancelTask(c *gin.Context) {
// UncancelTask handles POST /api/tasks/:id/uncancel/
func (h *TaskHandler) UncancelTask(c *gin.Context) {
user := c.MustGet(middleware.AuthUserKey).(*models.User)
userNow := middleware.GetUserNow(c)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
return
}
response, err := h.taskService.UncancelTask(uint(taskID), user.ID)
response, err := h.taskService.UncancelTask(uint(taskID), user.ID, userNow)
if err != nil {
switch {
case errors.Is(err, services.ErrTaskNotFound):
@@ -252,13 +262,15 @@ func (h *TaskHandler) UncancelTask(c *gin.Context) {
// ArchiveTask handles POST /api/tasks/:id/archive/
func (h *TaskHandler) ArchiveTask(c *gin.Context) {
user := c.MustGet(middleware.AuthUserKey).(*models.User)
userNow := middleware.GetUserNow(c)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
return
}
response, err := h.taskService.ArchiveTask(uint(taskID), user.ID)
response, err := h.taskService.ArchiveTask(uint(taskID), user.ID, userNow)
if err != nil {
switch {
case errors.Is(err, services.ErrTaskNotFound):
@@ -278,13 +290,15 @@ func (h *TaskHandler) ArchiveTask(c *gin.Context) {
// UnarchiveTask handles POST /api/tasks/:id/unarchive/
func (h *TaskHandler) UnarchiveTask(c *gin.Context) {
user := c.MustGet(middleware.AuthUserKey).(*models.User)
userNow := middleware.GetUserNow(c)
taskID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_task_id")})
return
}
response, err := h.taskService.UnarchiveTask(uint(taskID), user.ID)
response, err := h.taskService.UnarchiveTask(uint(taskID), user.ID, userNow)
if err != nil {
switch {
case errors.Is(err, services.ErrTaskNotFound):
@@ -389,6 +403,8 @@ func (h *TaskHandler) GetCompletion(c *gin.Context) {
// Supports both JSON and multipart form data (for image uploads)
func (h *TaskHandler) CreateCompletion(c *gin.Context) {
user := c.MustGet(middleware.AuthUserKey).(*models.User)
userNow := middleware.GetUserNow(c)
var req requests.CreateTaskCompletionRequest
contentType := c.GetHeader("Content-Type")
@@ -460,7 +476,7 @@ func (h *TaskHandler) CreateCompletion(c *gin.Context) {
}
}
response, err := h.taskService.CreateCompletion(&req, user.ID)
response, err := h.taskService.CreateCompletion(&req, user.ID, userNow)
if err != nil {
switch {
case errors.Is(err, services.ErrTaskNotFound):