-
-
+
+ updateField('in_progress', e.target.checked)}
+ className="h-4 w-4 rounded border-gray-300"
+ />
+
diff --git a/admin/src/app/(dashboard)/tasks/page.tsx b/admin/src/app/(dashboard)/tasks/page.tsx
index 73275fb..3d93010 100644
--- a/admin/src/app/(dashboard)/tasks/page.tsx
+++ b/admin/src/app/(dashboard)/tasks/page.tsx
@@ -58,9 +58,9 @@ const columns: ColumnDef[] = [
cell: ({ row }) => row.original.priority_name || '-',
},
{
- accessorKey: 'status_name',
- header: 'Status',
- cell: ({ row }) => row.original.status_name || '-',
+ accessorKey: 'in_progress',
+ header: 'In Progress',
+ cell: ({ row }) => row.original.in_progress ? '✓' : '—',
},
{
accessorKey: 'due_date',
diff --git a/admin/src/lib/api.ts b/admin/src/lib/api.ts
index 1235c14..35076c5 100644
--- a/admin/src/lib/api.ts
+++ b/admin/src/lib/api.ts
@@ -517,7 +517,6 @@ const createLookupApi = (endpoint: string) => ({
export const lookupsApi = {
categories: createLookupApi('categories'),
priorities: createLookupApi('priorities'),
- statuses: createLookupApi('statuses'),
frequencies: createLookupApi('frequencies'),
residenceTypes: createLookupApi('residence-types'),
specialties: createLookupApi('specialties'),
diff --git a/admin/src/types/models.ts b/admin/src/types/models.ts
index b8d8031..7416614 100644
--- a/admin/src/types/models.ts
+++ b/admin/src/types/models.ts
@@ -186,8 +186,7 @@ export interface Task {
category_name?: string;
priority_id?: number;
priority_name?: string;
- status_id?: number;
- status_name?: string;
+ in_progress: boolean;
frequency_id?: number;
frequency_name?: string;
due_date?: string;
@@ -211,7 +210,7 @@ export interface TaskListParams extends ListParams {
residence_id?: number;
category_id?: number;
priority_id?: number;
- status_id?: number;
+ in_progress?: boolean;
is_cancelled?: boolean;
is_archived?: boolean;
}
@@ -223,7 +222,7 @@ export interface CreateTaskRequest {
description?: string;
category_id?: number;
priority_id?: number;
- status_id?: number;
+ in_progress?: boolean;
frequency_id?: number;
assigned_to_id?: number;
due_date?: string;
@@ -239,7 +238,7 @@ export interface UpdateTaskRequest {
description?: string;
category_id?: number;
priority_id?: number;
- status_id?: number;
+ in_progress?: boolean;
frequency_id?: number;
due_date?: string;
next_due_date?: string;
diff --git a/internal/admin/dto/requests.go b/internal/admin/dto/requests.go
index 05afa14..39b2aab 100644
--- a/internal/admin/dto/requests.go
+++ b/internal/admin/dto/requests.go
@@ -118,7 +118,7 @@ type TaskFilters struct {
ResidenceID *uint `form:"residence_id"`
CategoryID *uint `form:"category_id"`
PriorityID *uint `form:"priority_id"`
- StatusID *uint `form:"status_id"`
+ InProgress *bool `form:"in_progress"`
IsCancelled *bool `form:"is_cancelled"`
IsArchived *bool `form:"is_archived"`
}
@@ -132,8 +132,8 @@ type UpdateTaskRequest struct {
Description *string `json:"description"`
CategoryID *uint `json:"category_id"`
PriorityID *uint `json:"priority_id"`
- StatusID *uint `json:"status_id"`
FrequencyID *uint `json:"frequency_id"`
+ InProgress *bool `json:"in_progress"`
DueDate *string `json:"due_date"`
NextDueDate *string `json:"next_due_date"`
EstimatedCost *float64 `json:"estimated_cost"`
@@ -265,18 +265,18 @@ type CreateResidenceRequest struct {
// CreateTaskRequest for creating a new task
type CreateTaskRequest struct {
- ResidenceID uint `json:"residence_id" binding:"required"`
- CreatedByID uint `json:"created_by_id" binding:"required"`
- Title string `json:"title" binding:"required,max=200"`
- Description string `json:"description"`
- CategoryID *uint `json:"category_id"`
- PriorityID *uint `json:"priority_id"`
- StatusID *uint `json:"status_id"`
- FrequencyID *uint `json:"frequency_id"`
- AssignedToID *uint `json:"assigned_to_id"`
- DueDate *string `json:"due_date"`
+ ResidenceID uint `json:"residence_id" binding:"required"`
+ CreatedByID uint `json:"created_by_id" binding:"required"`
+ Title string `json:"title" binding:"required,max=200"`
+ Description string `json:"description"`
+ CategoryID *uint `json:"category_id"`
+ PriorityID *uint `json:"priority_id"`
+ FrequencyID *uint `json:"frequency_id"`
+ InProgress bool `json:"in_progress"`
+ AssignedToID *uint `json:"assigned_to_id"`
+ DueDate *string `json:"due_date"`
EstimatedCost *float64 `json:"estimated_cost"`
- ContractorID *uint `json:"contractor_id"`
+ ContractorID *uint `json:"contractor_id"`
}
// CreateContractorRequest for creating a new contractor
diff --git a/internal/admin/dto/responses.go b/internal/admin/dto/responses.go
index 76eab24..c73475f 100644
--- a/internal/admin/dto/responses.go
+++ b/internal/admin/dto/responses.go
@@ -126,10 +126,9 @@ type TaskResponse struct {
CategoryName *string `json:"category_name,omitempty"`
PriorityID *uint `json:"priority_id,omitempty"`
PriorityName *string `json:"priority_name,omitempty"`
- StatusID *uint `json:"status_id,omitempty"`
- StatusName *string `json:"status_name,omitempty"`
FrequencyID *uint `json:"frequency_id,omitempty"`
FrequencyName *string `json:"frequency_name,omitempty"`
+ InProgress bool `json:"in_progress"`
DueDate *string `json:"due_date,omitempty"`
NextDueDate *string `json:"next_due_date,omitempty"`
EstimatedCost *float64 `json:"estimated_cost,omitempty"`
diff --git a/internal/admin/handlers/dashboard_handler.go b/internal/admin/handlers/dashboard_handler.go
index c1c00b2..4319c40 100644
--- a/internal/admin/handlers/dashboard_handler.go
+++ b/internal/admin/handlers/dashboard_handler.go
@@ -62,13 +62,6 @@ type TaskStats struct {
OnHold int64 `json:"on_hold"`
}
-// TaskStatusCount holds a single status count
-type TaskStatusCount struct {
- StatusID uint `json:"status_id"`
- StatusName string `json:"status_name"`
- Count int64 `json:"count"`
-}
-
// ContractorStats holds contractor-related statistics
type ContractorStats struct {
Total int64 `json:"total"`
@@ -123,14 +116,13 @@ func (h *AdminDashboardHandler) GetStats(c *gin.Context) {
h.db.Model(&models.Task{}).Scopes(scopes.ScopeCancelled).Count(&stats.Tasks.Cancelled)
h.db.Model(&models.Task{}).Where("created_at >= ?", thirtyDaysAgo).Count(&stats.Tasks.New30d)
- // Task counts by status (using LEFT JOIN to handle tasks with no status)
- // Note: These status counts use DB status names, not kanban categorization
+ // Task counts by in_progress flag
+ // Pending: active tasks that are not in progress and not completed
h.db.Model(&models.Task{}).
- Scopes(scopes.ScopeActive).
- Joins("LEFT JOIN task_taskstatus ON task_taskstatus.id = task_task.status_id").
- Where("LOWER(task_taskstatus.name) = ? OR task_taskstatus.id IS NULL", "pending").
+ Scopes(scopes.ScopeActive, scopes.ScopeNotInProgress, scopes.ScopeNotCompleted).
Count(&stats.Tasks.Pending)
+ // In Progress: active tasks with in_progress = true
h.db.Model(&models.Task{}).
Scopes(scopes.ScopeActive, scopes.ScopeInProgress).
Count(&stats.Tasks.InProgress)
@@ -141,11 +133,8 @@ func (h *AdminDashboardHandler) GetStats(c *gin.Context) {
Scopes(scopes.ScopeActive, scopes.ScopeCompleted).
Count(&stats.Tasks.Completed)
- h.db.Model(&models.Task{}).
- Scopes(scopes.ScopeActive).
- Joins("LEFT JOIN task_taskstatus ON task_taskstatus.id = task_task.status_id").
- Where("LOWER(task_taskstatus.name) = ?", "on hold").
- Count(&stats.Tasks.OnHold)
+ // OnHold: no longer used with in_progress boolean, set to 0
+ stats.Tasks.OnHold = 0
// Overdue: uses consistent logic from internal/task/scopes.ScopeOverdue
// Effective date (COALESCE(next_due_date, due_date)) < now, active, not completed
diff --git a/internal/admin/handlers/lookup_handler.go b/internal/admin/handlers/lookup_handler.go
index bf5d60b..eec1243 100644
--- a/internal/admin/handlers/lookup_handler.go
+++ b/internal/admin/handlers/lookup_handler.go
@@ -70,29 +70,6 @@ func (h *AdminLookupHandler) refreshPrioritiesCache(ctx context.Context) {
h.invalidateSeededDataCache(ctx)
}
-// refreshStatusesCache invalidates and refreshes the statuses cache
-func (h *AdminLookupHandler) refreshStatusesCache(ctx context.Context) {
- cache := services.GetCache()
- if cache == nil {
- return
- }
-
- var statuses []models.TaskStatus
- if err := h.db.Order("display_order ASC, name ASC").Find(&statuses).Error; err != nil {
- log.Warn().Err(err).Msg("Failed to fetch statuses for cache refresh")
- return
- }
-
- if err := cache.CacheStatuses(ctx, statuses); err != nil {
- log.Warn().Err(err).Msg("Failed to cache statuses")
- return
- }
- log.Debug().Int("count", len(statuses)).Msg("Refreshed statuses cache")
-
- // Invalidate unified seeded data cache
- h.invalidateSeededDataCache(ctx)
-}
-
// refreshFrequenciesCache invalidates and refreshes the frequencies cache
func (h *AdminLookupHandler) refreshFrequenciesCache(ctx context.Context) {
cache := services.GetCache()
@@ -471,149 +448,6 @@ func (h *AdminLookupHandler) DeletePriority(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Priority deleted successfully"})
}
-// ========== Task Statuses ==========
-
-type TaskStatusResponse struct {
- ID uint `json:"id"`
- Name string `json:"name"`
- Description string `json:"description"`
- Color string `json:"color"`
- DisplayOrder int `json:"display_order"`
-}
-
-type CreateUpdateStatusRequest struct {
- Name string `json:"name" binding:"required,max=20"`
- Description string `json:"description"`
- Color string `json:"color" binding:"max=7"`
- DisplayOrder *int `json:"display_order"`
-}
-
-func (h *AdminLookupHandler) ListStatuses(c *gin.Context) {
- var statuses []models.TaskStatus
- if err := h.db.Order("display_order ASC, name ASC").Find(&statuses).Error; err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch statuses"})
- return
- }
-
- responses := make([]TaskStatusResponse, len(statuses))
- for i, s := range statuses {
- responses[i] = TaskStatusResponse{
- ID: s.ID,
- Name: s.Name,
- Description: s.Description,
- Color: s.Color,
- DisplayOrder: s.DisplayOrder,
- }
- }
-
- c.JSON(http.StatusOK, gin.H{"data": responses, "total": len(responses)})
-}
-
-func (h *AdminLookupHandler) CreateStatus(c *gin.Context) {
- var req CreateUpdateStatusRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
-
- status := models.TaskStatus{
- Name: req.Name,
- Description: req.Description,
- Color: req.Color,
- }
- if req.DisplayOrder != nil {
- status.DisplayOrder = *req.DisplayOrder
- }
-
- if err := h.db.Create(&status).Error; err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create status"})
- return
- }
-
- // Refresh cache after creating
- h.refreshStatusesCache(c.Request.Context())
-
- c.JSON(http.StatusCreated, TaskStatusResponse{
- ID: status.ID,
- Name: status.Name,
- Description: status.Description,
- Color: status.Color,
- DisplayOrder: status.DisplayOrder,
- })
-}
-
-func (h *AdminLookupHandler) UpdateStatus(c *gin.Context) {
- id, err := strconv.ParseUint(c.Param("id"), 10, 32)
- if err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid status ID"})
- return
- }
-
- var status models.TaskStatus
- if err := h.db.First(&status, id).Error; err != nil {
- if err == gorm.ErrRecordNotFound {
- c.JSON(http.StatusNotFound, gin.H{"error": "Status not found"})
- return
- }
- c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch status"})
- return
- }
-
- var req CreateUpdateStatusRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
-
- status.Name = req.Name
- status.Description = req.Description
- status.Color = req.Color
- if req.DisplayOrder != nil {
- status.DisplayOrder = *req.DisplayOrder
- }
-
- if err := h.db.Save(&status).Error; err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update status"})
- return
- }
-
- // Refresh cache after updating
- h.refreshStatusesCache(c.Request.Context())
-
- c.JSON(http.StatusOK, TaskStatusResponse{
- ID: status.ID,
- Name: status.Name,
- Description: status.Description,
- Color: status.Color,
- DisplayOrder: status.DisplayOrder,
- })
-}
-
-func (h *AdminLookupHandler) DeleteStatus(c *gin.Context) {
- id, err := strconv.ParseUint(c.Param("id"), 10, 32)
- if err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid status ID"})
- return
- }
-
- var count int64
- h.db.Model(&models.Task{}).Where("status_id = ?", id).Count(&count)
- if count > 0 {
- c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete status that is in use by tasks"})
- return
- }
-
- if err := h.db.Delete(&models.TaskStatus{}, id).Error; err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete status"})
- return
- }
-
- // Refresh cache after deleting
- h.refreshStatusesCache(c.Request.Context())
-
- c.JSON(http.StatusOK, gin.H{"message": "Status deleted successfully"})
-}
-
// ========== Task Frequencies ==========
type TaskFrequencyResponse struct {
diff --git a/internal/admin/handlers/settings_handler.go b/internal/admin/handlers/settings_handler.go
index 0446d37..2f3f23e 100644
--- a/internal/admin/handlers/settings_handler.go
+++ b/internal/admin/handlers/settings_handler.go
@@ -143,16 +143,6 @@ func (h *AdminSettingsHandler) cacheAllLookups(ctx context.Context) (bool, error
}
log.Debug().Int("count", len(priorities)).Msg("Cached task priorities")
- // Fetch and cache task statuses
- var statuses []models.TaskStatus
- if err := h.db.Order("display_order ASC, name ASC").Find(&statuses).Error; err != nil {
- return false, fmt.Errorf("failed to fetch statuses: %w", err)
- }
- if err := cache.CacheStatuses(ctx, statuses); err != nil {
- return false, fmt.Errorf("failed to cache statuses: %w", err)
- }
- log.Debug().Int("count", len(statuses)).Msg("Cached task statuses")
-
// Fetch and cache task frequencies
var frequencies []models.TaskFrequency
if err := h.db.Order("display_order ASC, name ASC").Find(&frequencies).Error; err != nil {
@@ -203,7 +193,6 @@ func (h *AdminSettingsHandler) cacheAllLookups(ctx context.Context) (bool, error
"task_categories": categories,
"task_priorities": priorities,
"task_frequencies": frequencies,
- "task_statuses": statuses,
"contractor_specialties": specialties,
"task_templates": buildGroupedTemplates(taskTemplates),
}
diff --git a/internal/admin/handlers/task_handler.go b/internal/admin/handlers/task_handler.go
index b1dd713..b69c220 100644
--- a/internal/admin/handlers/task_handler.go
+++ b/internal/admin/handlers/task_handler.go
@@ -38,8 +38,7 @@ func (h *AdminTaskHandler) List(c *gin.Context) {
Preload("Residence").
Preload("CreatedBy").
Preload("Category").
- Preload("Priority").
- Preload("Status")
+ Preload("Priority")
// Apply search
if filters.Search != "" {
@@ -57,8 +56,8 @@ func (h *AdminTaskHandler) List(c *gin.Context) {
if filters.PriorityID != nil {
query = query.Where("priority_id = ?", *filters.PriorityID)
}
- if filters.StatusID != nil {
- query = query.Where("status_id = ?", *filters.StatusID)
+ if filters.InProgress != nil {
+ query = query.Where("in_progress = ?", *filters.InProgress)
}
if filters.IsCancelled != nil {
query = query.Where("is_cancelled = ?", *filters.IsCancelled)
@@ -109,7 +108,6 @@ func (h *AdminTaskHandler) Get(c *gin.Context) {
Preload("AssignedTo").
Preload("Category").
Preload("Priority").
- Preload("Status").
Preload("Frequency").
Preload("Completions").
First(&task, id).Error; err != nil {
@@ -210,8 +208,8 @@ func (h *AdminTaskHandler) Update(c *gin.Context) {
if req.PriorityID != nil {
updates["priority_id"] = *req.PriorityID
}
- if req.StatusID != nil {
- updates["status_id"] = *req.StatusID
+ if req.InProgress != nil {
+ updates["in_progress"] = *req.InProgress
}
if req.FrequencyID != nil {
updates["frequency_id"] = *req.FrequencyID
@@ -254,7 +252,7 @@ func (h *AdminTaskHandler) Update(c *gin.Context) {
}
// Reload with preloads for response
- h.db.Preload("Residence").Preload("CreatedBy").Preload("Category").Preload("Priority").Preload("Status").First(&task, id)
+ h.db.Preload("Residence").Preload("CreatedBy").Preload("Category").Preload("Priority").First(&task, id)
c.JSON(http.StatusOK, h.toTaskResponse(&task))
}
@@ -287,7 +285,7 @@ func (h *AdminTaskHandler) Create(c *gin.Context) {
Description: req.Description,
CategoryID: req.CategoryID,
PriorityID: req.PriorityID,
- StatusID: req.StatusID,
+ InProgress: req.InProgress,
FrequencyID: req.FrequencyID,
AssignedToID: req.AssignedToID,
ContractorID: req.ContractorID,
@@ -311,7 +309,7 @@ func (h *AdminTaskHandler) Create(c *gin.Context) {
return
}
- h.db.Preload("Residence").Preload("CreatedBy").Preload("Category").Preload("Priority").Preload("Status").First(&task, task.ID)
+ h.db.Preload("Residence").Preload("CreatedBy").Preload("Category").Preload("Priority").First(&task, task.ID)
c.JSON(http.StatusCreated, h.toTaskResponse(&task))
}
@@ -336,7 +334,7 @@ func (h *AdminTaskHandler) Delete(c *gin.Context) {
// Soft delete - archive and cancel
task.IsArchived = true
task.IsCancelled = true
- if err := h.db.Omit("Residence", "CreatedBy", "AssignedTo", "Category", "Priority", "Status", "Frequency", "ParentTask", "Completions").Save(&task).Error; err != nil {
+ if err := h.db.Omit("Residence", "CreatedBy", "AssignedTo", "Category", "Priority", "Frequency", "ParentTask", "Completions").Save(&task).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete task"})
return
}
@@ -373,7 +371,7 @@ func (h *AdminTaskHandler) toTaskResponse(task *models.Task) dto.TaskResponse {
Description: task.Description,
CategoryID: task.CategoryID,
PriorityID: task.PriorityID,
- StatusID: task.StatusID,
+ InProgress: task.InProgress,
FrequencyID: task.FrequencyID,
ContractorID: task.ContractorID,
ParentTaskID: task.ParentTaskID,
@@ -401,9 +399,6 @@ func (h *AdminTaskHandler) toTaskResponse(task *models.Task) dto.TaskResponse {
if task.Priority != nil {
response.PriorityName = &task.Priority.Name
}
- if task.Status != nil {
- response.StatusName = &task.Status.Name
- }
if task.Frequency != nil {
response.FrequencyName = &task.Frequency.Name
}
diff --git a/internal/admin/routes.go b/internal/admin/routes.go
index 02cebcc..b43956e 100644
--- a/internal/admin/routes.go
+++ b/internal/admin/routes.go
@@ -263,15 +263,6 @@ func SetupRoutes(router *gin.Engine, db *gorm.DB, cfg *config.Config, deps *Depe
priorities.DELETE("/:id", lookupHandler.DeletePriority)
}
- // Task Statuses
- statuses := protected.Group("/lookups/statuses")
- {
- statuses.GET("", lookupHandler.ListStatuses)
- statuses.POST("", lookupHandler.CreateStatus)
- statuses.PUT("/:id", lookupHandler.UpdateStatus)
- statuses.DELETE("/:id", lookupHandler.DeleteStatus)
- }
-
// Task Frequencies
frequencies := protected.Group("/lookups/frequencies")
{
diff --git a/internal/database/database.go b/internal/database/database.go
index bdad7c0..7d34ebb 100644
--- a/internal/database/database.go
+++ b/internal/database/database.go
@@ -114,7 +114,6 @@ func Migrate() error {
&models.TaskCategory{},
&models.TaskPriority{},
&models.TaskFrequency{},
- &models.TaskStatus{},
&models.ContractorSpecialty{},
&models.TaskTemplate{}, // Task templates reference category and frequency
diff --git a/internal/dto/requests/task.go b/internal/dto/requests/task.go
index f77cf13..ffeec11 100644
--- a/internal/dto/requests/task.go
+++ b/internal/dto/requests/task.go
@@ -59,8 +59,8 @@ type CreateTaskRequest struct {
Description string `json:"description"`
CategoryID *uint `json:"category_id"`
PriorityID *uint `json:"priority_id"`
- StatusID *uint `json:"status_id"`
FrequencyID *uint `json:"frequency_id"`
+ InProgress bool `json:"in_progress"`
AssignedToID *uint `json:"assigned_to_id"`
DueDate *FlexibleDate `json:"due_date"`
EstimatedCost *decimal.Decimal `json:"estimated_cost"`
@@ -73,8 +73,8 @@ type UpdateTaskRequest struct {
Description *string `json:"description"`
CategoryID *uint `json:"category_id"`
PriorityID *uint `json:"priority_id"`
- StatusID *uint `json:"status_id"`
FrequencyID *uint `json:"frequency_id"`
+ InProgress *bool `json:"in_progress"`
AssignedToID *uint `json:"assigned_to_id"`
DueDate *FlexibleDate `json:"due_date"`
EstimatedCost *decimal.Decimal `json:"estimated_cost"`
diff --git a/internal/dto/responses/task.go b/internal/dto/responses/task.go
index 78c6811..7bbedd7 100644
--- a/internal/dto/responses/task.go
+++ b/internal/dto/responses/task.go
@@ -29,15 +29,6 @@ type TaskPriorityResponse struct {
DisplayOrder int `json:"display_order"`
}
-// TaskStatusResponse represents a task status
-type TaskStatusResponse struct {
- ID uint `json:"id"`
- Name string `json:"name"`
- Description string `json:"description"`
- Color string `json:"color"`
- DisplayOrder int `json:"display_order"`
-}
-
// TaskFrequencyResponse represents a task frequency
type TaskFrequencyResponse struct {
ID uint `json:"id"`
@@ -91,10 +82,9 @@ type TaskResponse struct {
Category *TaskCategoryResponse `json:"category,omitempty"`
PriorityID *uint `json:"priority_id"`
Priority *TaskPriorityResponse `json:"priority,omitempty"`
- StatusID *uint `json:"status_id"`
- Status *TaskStatusResponse `json:"status,omitempty"`
FrequencyID *uint `json:"frequency_id"`
Frequency *TaskFrequencyResponse `json:"frequency,omitempty"`
+ InProgress bool `json:"in_progress"`
DueDate *time.Time `json:"due_date"`
NextDueDate *time.Time `json:"next_due_date"` // For recurring tasks, updated after each completion
EstimatedCost *decimal.Decimal `json:"estimated_cost"`
@@ -163,20 +153,6 @@ func NewTaskPriorityResponse(p *models.TaskPriority) *TaskPriorityResponse {
}
}
-// NewTaskStatusResponse creates a TaskStatusResponse from a model
-func NewTaskStatusResponse(s *models.TaskStatus) *TaskStatusResponse {
- if s == nil {
- return nil
- }
- return &TaskStatusResponse{
- ID: s.ID,
- Name: s.Name,
- Description: s.Description,
- Color: s.Color,
- DisplayOrder: s.DisplayOrder,
- }
-}
-
// NewTaskFrequencyResponse creates a TaskFrequencyResponse from a model
func NewTaskFrequencyResponse(f *models.TaskFrequency) *TaskFrequencyResponse {
if f == nil {
@@ -247,8 +223,8 @@ func NewTaskResponseWithThreshold(t *models.Task, daysThreshold int) TaskRespons
Description: t.Description,
CategoryID: t.CategoryID,
PriorityID: t.PriorityID,
- StatusID: t.StatusID,
FrequencyID: t.FrequencyID,
+ InProgress: t.InProgress,
AssignedToID: t.AssignedToID,
DueDate: t.DueDate,
NextDueDate: t.NextDueDate,
@@ -276,9 +252,6 @@ func NewTaskResponseWithThreshold(t *models.Task, daysThreshold int) TaskRespons
if t.Priority != nil {
resp.Priority = NewTaskPriorityResponse(t.Priority)
}
- if t.Status != nil {
- resp.Status = NewTaskStatusResponse(t.Status)
- }
if t.Frequency != nil {
resp.Frequency = NewTaskFrequencyResponse(t.Frequency)
}
diff --git a/internal/handlers/static_data_handler.go b/internal/handlers/static_data_handler.go
index 513133c..93c96e4 100644
--- a/internal/handlers/static_data_handler.go
+++ b/internal/handlers/static_data_handler.go
@@ -15,13 +15,12 @@ import (
// SeededDataResponse represents the unified seeded data response
type SeededDataResponse struct {
- ResidenceTypes interface{} `json:"residence_types"`
- TaskCategories interface{} `json:"task_categories"`
- TaskPriorities interface{} `json:"task_priorities"`
- TaskFrequencies interface{} `json:"task_frequencies"`
- TaskStatuses interface{} `json:"task_statuses"`
- ContractorSpecialties interface{} `json:"contractor_specialties"`
- TaskTemplates responses.TaskTemplatesGroupedResponse `json:"task_templates"`
+ ResidenceTypes interface{} `json:"residence_types"`
+ TaskCategories interface{} `json:"task_categories"`
+ TaskPriorities interface{} `json:"task_priorities"`
+ TaskFrequencies interface{} `json:"task_frequencies"`
+ ContractorSpecialties interface{} `json:"contractor_specialties"`
+ TaskTemplates responses.TaskTemplatesGroupedResponse `json:"task_templates"`
}
// StaticDataHandler handles static/lookup data endpoints
@@ -113,12 +112,6 @@ func (h *StaticDataHandler) GetStaticData(c *gin.Context) {
return
}
- taskStatuses, err := h.taskService.GetStatuses()
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_fetch_task_statuses")})
- return
- }
-
contractorSpecialties, err := h.contractorService.GetSpecialties()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_fetch_contractor_specialties")})
@@ -137,7 +130,6 @@ func (h *StaticDataHandler) GetStaticData(c *gin.Context) {
TaskCategories: taskCategories,
TaskPriorities: taskPriorities,
TaskFrequencies: taskFrequencies,
- TaskStatuses: taskStatuses,
ContractorSpecialties: contractorSpecialties,
TaskTemplates: taskTemplates,
}
diff --git a/internal/handlers/task_handler.go b/internal/handlers/task_handler.go
index 2bbcd6c..d4efa37 100644
--- a/internal/handlers/task_handler.go
+++ b/internal/handlers/task_handler.go
@@ -517,16 +517,6 @@ func (h *TaskHandler) GetPriorities(c *gin.Context) {
c.JSON(http.StatusOK, priorities)
}
-// GetStatuses handles GET /api/tasks/statuses/
-func (h *TaskHandler) GetStatuses(c *gin.Context) {
- statuses, err := h.taskService.GetStatuses()
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- return
- }
- c.JSON(http.StatusOK, statuses)
-}
-
// GetFrequencies handles GET /api/tasks/frequencies/
func (h *TaskHandler) GetFrequencies(c *gin.Context) {
frequencies, err := h.taskService.GetFrequencies()
diff --git a/internal/handlers/task_handler_test.go b/internal/handlers/task_handler_test.go
index b777d3d..91ec303 100644
--- a/internal/handlers/task_handler_test.go
+++ b/internal/handlers/task_handler_test.go
@@ -610,7 +610,6 @@ func TestTaskHandler_GetLookups(t *testing.T) {
authGroup.Use(testutil.MockAuthMiddleware(user))
authGroup.GET("/categories/", handler.GetCategories)
authGroup.GET("/priorities/", handler.GetPriorities)
- authGroup.GET("/statuses/", handler.GetStatuses)
authGroup.GET("/frequencies/", handler.GetFrequencies)
t.Run("get categories", func(t *testing.T) {
@@ -642,18 +641,6 @@ func TestTaskHandler_GetLookups(t *testing.T) {
assert.Contains(t, response[0], "level")
})
- t.Run("get statuses", func(t *testing.T) {
- w := testutil.MakeRequest(router, "GET", "/api/tasks/statuses/", nil, "test-token")
-
- testutil.AssertStatusCode(t, w, http.StatusOK)
-
- var response []map[string]interface{}
- err := json.Unmarshal(w.Body.Bytes(), &response)
- require.NoError(t, err)
-
- assert.Greater(t, len(response), 0)
- })
-
t.Run("get frequencies", func(t *testing.T) {
w := testutil.MakeRequest(router, "GET", "/api/tasks/frequencies/", nil, "test-token")
diff --git a/internal/integration/contractor_sharing_test.go b/internal/integration/contractor_sharing_test.go
index 7d989d8..c67d241 100644
--- a/internal/integration/contractor_sharing_test.go
+++ b/internal/integration/contractor_sharing_test.go
@@ -32,7 +32,8 @@ func TestIntegration_ContractorSharingFlow(t *testing.T) {
var residenceResp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &residenceResp)
require.NoError(t, err)
- residenceCID := residenceResp["id"].(float64)
+ residenceData := residenceResp["data"].(map[string]interface{})
+ residenceCID := residenceData["id"].(float64)
// ========== User A shares residence C with User B ==========
// Generate share code
@@ -191,7 +192,8 @@ func TestIntegration_ContractorAccessWithoutResidenceShare(t *testing.T) {
var residenceResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &residenceResp)
- residenceID := residenceResp["id"].(float64)
+ residenceData := residenceResp["data"].(map[string]interface{})
+ residenceID := residenceData["id"].(float64)
// User A creates a contractor tied to the residence (NOT shared with User B)
contractorBody := map[string]interface{}{
@@ -235,9 +237,10 @@ func TestIntegration_ContractorUpdateAndDeleteAccess(t *testing.T) {
w := app.makeAuthenticatedRequest(t, "POST", "/api/residences", residenceBody, userAToken)
require.Equal(t, http.StatusCreated, w.Code)
- var residenceResp map[string]interface{}
- json.Unmarshal(w.Body.Bytes(), &residenceResp)
- residenceID := residenceResp["id"].(float64)
+ var residenceResp2 map[string]interface{}
+ json.Unmarshal(w.Body.Bytes(), &residenceResp2)
+ residenceData2 := residenceResp2["data"].(map[string]interface{})
+ residenceID := residenceData2["id"].(float64)
// Share with User B
w = app.makeAuthenticatedRequest(t, "POST", "/api/residences/"+formatID(residenceID)+"/generate-share-code", nil, userAToken)
@@ -259,9 +262,9 @@ func TestIntegration_ContractorUpdateAndDeleteAccess(t *testing.T) {
w = app.makeAuthenticatedRequest(t, "POST", "/api/contractors", contractorBody, userAToken)
require.Equal(t, http.StatusCreated, w.Code)
- var contractorResp map[string]interface{}
- json.Unmarshal(w.Body.Bytes(), &contractorResp)
- contractorID := contractorResp["id"].(float64)
+ var contractorResp3 map[string]interface{}
+ json.Unmarshal(w.Body.Bytes(), &contractorResp3)
+ contractorID3 := contractorResp3["id"].(float64)
// User B (with access) can update the contractor
// Note: Must include residence_id to keep it tied to the residence
@@ -269,7 +272,7 @@ func TestIntegration_ContractorUpdateAndDeleteAccess(t *testing.T) {
"name": "Updated by User B",
"residence_id": uint(residenceID),
}
- w = app.makeAuthenticatedRequest(t, "PUT", "/api/contractors/"+formatID(contractorID), updateBody, userBToken)
+ w = app.makeAuthenticatedRequest(t, "PUT", "/api/contractors/"+formatID(contractorID3), updateBody, userBToken)
assert.Equal(t, http.StatusOK, w.Code, "User B should be able to update contractor in shared residence")
// User C (without access) cannot update the contractor
@@ -277,15 +280,15 @@ func TestIntegration_ContractorUpdateAndDeleteAccess(t *testing.T) {
"name": "Hacked by User C",
"residence_id": uint(residenceID),
}
- w = app.makeAuthenticatedRequest(t, "PUT", "/api/contractors/"+formatID(contractorID), updateBody2, userCToken)
+ w = app.makeAuthenticatedRequest(t, "PUT", "/api/contractors/"+formatID(contractorID3), updateBody2, userCToken)
assert.Equal(t, http.StatusForbidden, w.Code, "User C should NOT be able to update contractor")
// User C cannot delete the contractor
- w = app.makeAuthenticatedRequest(t, "DELETE", "/api/contractors/"+formatID(contractorID), nil, userCToken)
+ w = app.makeAuthenticatedRequest(t, "DELETE", "/api/contractors/"+formatID(contractorID3), nil, userCToken)
assert.Equal(t, http.StatusForbidden, w.Code, "User C should NOT be able to delete contractor")
// User B (with access) can delete the contractor
- w = app.makeAuthenticatedRequest(t, "DELETE", "/api/contractors/"+formatID(contractorID), nil, userBToken)
+ w = app.makeAuthenticatedRequest(t, "DELETE", "/api/contractors/"+formatID(contractorID3), nil, userBToken)
assert.Equal(t, http.StatusOK, w.Code, "User B should be able to delete contractor in shared residence")
}
diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go
index f5d4d25..2cdee99 100644
--- a/internal/integration/integration_test.go
+++ b/internal/integration/integration_test.go
@@ -125,7 +125,6 @@ func setupIntegrationTest(t *testing.T) *TestApp {
api.GET("/task-categories", taskHandler.GetCategories)
api.GET("/task-priorities", taskHandler.GetPriorities)
- api.GET("/task-statuses", taskHandler.GetStatuses)
api.GET("/task-frequencies", taskHandler.GetFrequencies)
}
@@ -334,10 +333,11 @@ func TestIntegration_ResidenceFlow(t *testing.T) {
var createResp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &createResp)
require.NoError(t, err)
- residenceID := createResp["id"].(float64)
+ createData := createResp["data"].(map[string]interface{})
+ residenceID := createData["id"].(float64)
assert.NotZero(t, residenceID)
- assert.Equal(t, "My House", createResp["name"])
- assert.True(t, createResp["is_primary"].(bool))
+ assert.Equal(t, "My House", createData["name"])
+ assert.True(t, createData["is_primary"].(bool))
// 2. Get the residence
w = app.makeAuthenticatedRequest(t, "GET", "/api/residences/"+formatID(residenceID), nil, token)
@@ -368,8 +368,9 @@ func TestIntegration_ResidenceFlow(t *testing.T) {
var updateResp map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &updateResp)
require.NoError(t, err)
- assert.Equal(t, "My Updated House", updateResp["name"])
- assert.Equal(t, "Dallas", updateResp["city"])
+ updateData := updateResp["data"].(map[string]interface{})
+ assert.Equal(t, "My Updated House", updateData["name"])
+ assert.Equal(t, "Dallas", updateData["city"])
// 5. Delete the residence (returns 200 with message, not 204)
w = app.makeAuthenticatedRequest(t, "DELETE", "/api/residences/"+formatID(residenceID), nil, token)
@@ -396,7 +397,8 @@ func TestIntegration_ResidenceSharingFlow(t *testing.T) {
var createResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &createResp)
- residenceID := createResp["id"].(float64)
+ createData := createResp["data"].(map[string]interface{})
+ residenceID := createData["id"].(float64)
// Other user cannot access initially
w = app.makeAuthenticatedRequest(t, "GET", "/api/residences/"+formatID(residenceID), nil, userToken)
@@ -448,7 +450,8 @@ func TestIntegration_TaskFlow(t *testing.T) {
var residenceResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &residenceResp)
- residenceID := uint(residenceResp["id"].(float64))
+ residenceData := residenceResp["data"].(map[string]interface{})
+ residenceID := uint(residenceData["id"].(float64))
// 1. Create a task
taskBody := map[string]interface{}{
@@ -461,9 +464,10 @@ func TestIntegration_TaskFlow(t *testing.T) {
var taskResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &taskResp)
- taskID := taskResp["id"].(float64)
+ taskData := taskResp["data"].(map[string]interface{})
+ taskID := taskData["id"].(float64)
assert.NotZero(t, taskID)
- assert.Equal(t, "Fix leaky faucet", taskResp["title"])
+ assert.Equal(t, "Fix leaky faucet", taskData["title"])
// 2. Get the task
w = app.makeAuthenticatedRequest(t, "GET", "/api/tasks/"+formatID(taskID), nil, token)
@@ -477,9 +481,10 @@ func TestIntegration_TaskFlow(t *testing.T) {
w = app.makeAuthenticatedRequest(t, "PUT", "/api/tasks/"+formatID(taskID), updateBody, token)
assert.Equal(t, http.StatusOK, w.Code)
- var updateResp map[string]interface{}
- json.Unmarshal(w.Body.Bytes(), &updateResp)
- assert.Equal(t, "Fix kitchen faucet", updateResp["title"])
+ var taskUpdateResp map[string]interface{}
+ json.Unmarshal(w.Body.Bytes(), &taskUpdateResp)
+ taskUpdateData := taskUpdateResp["data"].(map[string]interface{})
+ assert.Equal(t, "Fix kitchen faucet", taskUpdateData["title"])
// 4. Mark as in progress
w = app.makeAuthenticatedRequest(t, "POST", "/api/tasks/"+formatID(taskID)+"/mark-in-progress", nil, token)
@@ -487,9 +492,8 @@ func TestIntegration_TaskFlow(t *testing.T) {
var progressResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &progressResp)
- task := progressResp["task"].(map[string]interface{})
- status := task["status"].(map[string]interface{})
- assert.Equal(t, "In Progress", status["name"])
+ progressData := progressResp["data"].(map[string]interface{})
+ assert.True(t, progressData["in_progress"].(bool))
// 5. Complete the task
completionBody := map[string]interface{}{
@@ -501,9 +505,10 @@ func TestIntegration_TaskFlow(t *testing.T) {
var completionResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &completionResp)
- completionID := completionResp["id"].(float64)
+ completionData := completionResp["data"].(map[string]interface{})
+ completionID := completionData["id"].(float64)
assert.NotZero(t, completionID)
- assert.Equal(t, "Fixed the faucet", completionResp["notes"])
+ assert.Equal(t, "Fixed the faucet", completionData["notes"])
// 6. List completions
w = app.makeAuthenticatedRequest(t, "GET", "/api/completions?task_id="+formatID(taskID), nil, token)
@@ -515,8 +520,8 @@ func TestIntegration_TaskFlow(t *testing.T) {
var archiveResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &archiveResp)
- archivedTask := archiveResp["task"].(map[string]interface{})
- assert.True(t, archivedTask["is_archived"].(bool))
+ archivedData := archiveResp["data"].(map[string]interface{})
+ assert.True(t, archivedData["is_archived"].(bool))
// 8. Unarchive the task
w = app.makeAuthenticatedRequest(t, "POST", "/api/tasks/"+formatID(taskID)+"/unarchive", nil, token)
@@ -528,8 +533,8 @@ func TestIntegration_TaskFlow(t *testing.T) {
var cancelResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &cancelResp)
- cancelledTask := cancelResp["task"].(map[string]interface{})
- assert.True(t, cancelledTask["is_cancelled"].(bool))
+ cancelledData := cancelResp["data"].(map[string]interface{})
+ assert.True(t, cancelledData["is_cancelled"].(bool))
// 10. Delete the task (returns 200 with message, not 204)
w = app.makeAuthenticatedRequest(t, "DELETE", "/api/tasks/"+formatID(taskID), nil, token)
@@ -547,7 +552,8 @@ func TestIntegration_TasksByResidenceKanban(t *testing.T) {
var residenceResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &residenceResp)
- residenceID := uint(residenceResp["id"].(float64))
+ residenceData := residenceResp["data"].(map[string]interface{})
+ residenceID := uint(residenceData["id"].(float64))
// Create multiple tasks
for i := 1; i <= 3; i++ {
@@ -592,7 +598,6 @@ func TestIntegration_LookupEndpoints(t *testing.T) {
{"residence types", "/api/residence-types"},
{"task categories", "/api/task-categories"},
{"task priorities", "/api/task-priorities"},
- {"task statuses", "/api/task-statuses"},
{"task frequencies", "/api/task-frequencies"},
}
@@ -633,7 +638,8 @@ func TestIntegration_CrossUserAccessDenied(t *testing.T) {
var residenceResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &residenceResp)
- residenceID := residenceResp["id"].(float64)
+ residenceData := residenceResp["data"].(map[string]interface{})
+ residenceID := residenceData["id"].(float64)
// User1 creates a task
taskBody := map[string]interface{}{
@@ -645,7 +651,8 @@ func TestIntegration_CrossUserAccessDenied(t *testing.T) {
var taskResp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &taskResp)
- taskID := taskResp["id"].(float64)
+ taskData := taskResp["data"].(map[string]interface{})
+ taskID := taskData["id"].(float64)
// User2 cannot access User1's residence
w = app.makeAuthenticatedRequest(t, "GET", "/api/residences/"+formatID(residenceID), nil, user2Token)
@@ -693,7 +700,12 @@ func TestIntegration_ResponseStructure(t *testing.T) {
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
- // Verify all expected fields are present
+ // Response is wrapped with "data" and "summary"
+ data := resp["data"].(map[string]interface{})
+ _, hasSummary := resp["summary"]
+ assert.True(t, hasSummary, "Expected 'summary' field in response")
+
+ // Verify all expected fields are present in data
expectedFields := []string{
"id", "owner_id", "name", "street_address", "city",
"state_province", "postal_code", "country",
@@ -701,13 +713,13 @@ func TestIntegration_ResponseStructure(t *testing.T) {
}
for _, field := range expectedFields {
- _, exists := resp[field]
- assert.True(t, exists, "Expected field %s to be present", field)
+ _, exists := data[field]
+ assert.True(t, exists, "Expected field %s to be present in data", field)
}
// Check that nullable fields can be null
- assert.Nil(t, resp["bedrooms"])
- assert.Nil(t, resp["bathrooms"])
+ assert.Nil(t, data["bedrooms"])
+ assert.Nil(t, data["bathrooms"])
}
// ============ Helper Functions ============
diff --git a/internal/models/task.go b/internal/models/task.go
index 7983f95..bc7e46e 100644
--- a/internal/models/task.go
+++ b/internal/models/task.go
@@ -35,20 +35,6 @@ func (TaskPriority) TableName() string {
return "task_taskpriority"
}
-// TaskStatus represents the task_taskstatus table
-type TaskStatus struct {
- BaseModel
- Name string `gorm:"column:name;size:20;not null" json:"name"`
- Description string `gorm:"column:description;type:text" json:"description"`
- Color string `gorm:"column:color;size:7" json:"color"`
- DisplayOrder int `gorm:"column:display_order;default:0" json:"display_order"`
-}
-
-// TableName returns the table name for GORM
-func (TaskStatus) TableName() string {
- return "task_taskstatus"
-}
-
// TaskFrequency represents the task_taskfrequency table
type TaskFrequency struct {
BaseModel
@@ -79,11 +65,12 @@ type Task struct {
Category *TaskCategory `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
PriorityID *uint `gorm:"column:priority_id;index" json:"priority_id"`
Priority *TaskPriority `gorm:"foreignKey:PriorityID" json:"priority,omitempty"`
- StatusID *uint `gorm:"column:status_id;index" json:"status_id"`
- Status *TaskStatus `gorm:"foreignKey:StatusID" json:"status,omitempty"`
FrequencyID *uint `gorm:"column:frequency_id;index" json:"frequency_id"`
Frequency *TaskFrequency `gorm:"foreignKey:FrequencyID" json:"frequency,omitempty"`
+ // In Progress flag - replaces status lookup
+ InProgress bool `gorm:"column:in_progress;default:false;index" json:"in_progress"`
+
DueDate *time.Time `gorm:"column:due_date;type:date;index" json:"due_date"`
NextDueDate *time.Time `gorm:"column:next_due_date;type:date;index" json:"next_due_date"` // For recurring tasks, updated after each completion
EstimatedCost *decimal.Decimal `gorm:"column:estimated_cost;type:decimal(10,2)" json:"estimated_cost"`
diff --git a/internal/models/task_test.go b/internal/models/task_test.go
index 0adb263..3e49792 100644
--- a/internal/models/task_test.go
+++ b/internal/models/task_test.go
@@ -24,11 +24,6 @@ func TestTaskPriority_TableName(t *testing.T) {
assert.Equal(t, "task_taskpriority", p.TableName())
}
-func TestTaskStatus_TableName(t *testing.T) {
- s := TaskStatus{}
- assert.Equal(t, "task_taskstatus", s.TableName())
-}
-
func TestTaskFrequency_TableName(t *testing.T) {
f := TaskFrequency{}
assert.Equal(t, "task_taskfrequency", f.TableName())
@@ -134,28 +129,6 @@ func TestTaskPriority_JSONSerialization(t *testing.T) {
assert.Equal(t, "#e74c3c", result["color"])
}
-func TestTaskStatus_JSONSerialization(t *testing.T) {
- status := TaskStatus{
- Name: "In Progress",
- Description: "Task is being worked on",
- Color: "#3498db",
- DisplayOrder: 2,
- }
- status.ID = 2
-
- data, err := json.Marshal(status)
- assert.NoError(t, err)
-
- var result map[string]interface{}
- err = json.Unmarshal(data, &result)
- assert.NoError(t, err)
-
- assert.Equal(t, float64(2), result["id"])
- assert.Equal(t, "In Progress", result["name"])
- assert.Equal(t, "Task is being worked on", result["description"])
- assert.Equal(t, "#3498db", result["color"])
-}
-
func TestTaskFrequency_JSONSerialization(t *testing.T) {
days := 7
freq := TaskFrequency{
diff --git a/internal/repositories/task_repo.go b/internal/repositories/task_repo.go
index 8f66c72..185fbdf 100644
--- a/internal/repositories/task_repo.go
+++ b/internal/repositories/task_repo.go
@@ -30,7 +30,6 @@ func (r *TaskRepository) FindByID(id uint) (*models.Task, error) {
Preload("AssignedTo").
Preload("Category").
Preload("Priority").
- Preload("Status").
Preload("Frequency").
Preload("Completions").
Preload("Completions.Images").
@@ -49,7 +48,6 @@ func (r *TaskRepository) FindByResidence(residenceID uint) ([]models.Task, error
Preload("AssignedTo").
Preload("Category").
Preload("Priority").
- Preload("Status").
Preload("Frequency").
Preload("Completions").
Preload("Completions.Images").
@@ -68,7 +66,6 @@ func (r *TaskRepository) FindByUser(userID uint, residenceIDs []uint) ([]models.
Preload("AssignedTo").
Preload("Category").
Preload("Priority").
- Preload("Status").
Preload("Frequency").
Preload("Completions").
Preload("Completions.Images").
@@ -87,7 +84,7 @@ func (r *TaskRepository) Create(task *models.Task) error {
// Update updates a task
// Uses Omit to exclude associations that shouldn't be updated via Save
func (r *TaskRepository) Update(task *models.Task) error {
- return r.db.Omit("Residence", "CreatedBy", "AssignedTo", "Category", "Priority", "Status", "Frequency", "ParentTask", "Completions").Save(task).Error
+ return r.db.Omit("Residence", "CreatedBy", "AssignedTo", "Category", "Priority", "Frequency", "ParentTask", "Completions").Save(task).Error
}
// Delete hard-deletes a task
@@ -98,10 +95,10 @@ func (r *TaskRepository) Delete(id uint) error {
// === Task State Operations ===
// MarkInProgress marks a task as in progress
-func (r *TaskRepository) MarkInProgress(id uint, statusID uint) error {
+func (r *TaskRepository) MarkInProgress(id uint) error {
return r.db.Model(&models.Task{}).
Where("id = ?", id).
- Update("status_id", statusID).Error
+ Update("in_progress", true).Error
}
// Cancel cancels a task
@@ -142,7 +139,6 @@ func (r *TaskRepository) GetKanbanData(residenceID uint, daysThreshold int) (*mo
Preload("AssignedTo").
Preload("Category").
Preload("Priority").
- Preload("Status").
Preload("Frequency").
Preload("Completions").
Preload("Completions.Images").
@@ -229,7 +225,6 @@ func (r *TaskRepository) GetKanbanDataForMultipleResidences(residenceIDs []uint,
Preload("AssignedTo").
Preload("Category").
Preload("Priority").
- Preload("Status").
Preload("Frequency").
Preload("Completions").
Preload("Completions.Images").
@@ -325,13 +320,6 @@ func (r *TaskRepository) GetAllPriorities() ([]models.TaskPriority, error) {
return priorities, err
}
-// GetAllStatuses returns all task statuses
-func (r *TaskRepository) GetAllStatuses() ([]models.TaskStatus, error) {
- var statuses []models.TaskStatus
- err := r.db.Order("display_order").Find(&statuses).Error
- return statuses, err
-}
-
// GetAllFrequencies returns all task frequencies
func (r *TaskRepository) GetAllFrequencies() ([]models.TaskFrequency, error) {
var frequencies []models.TaskFrequency
@@ -339,16 +327,6 @@ func (r *TaskRepository) GetAllFrequencies() ([]models.TaskFrequency, error) {
return frequencies, err
}
-// FindStatusByName finds a status by name
-func (r *TaskRepository) FindStatusByName(name string) (*models.TaskStatus, error) {
- var status models.TaskStatus
- err := r.db.Where("name = ?", name).First(&status).Error
- if err != nil {
- return nil, err
- }
- return &status, nil
-}
-
// CountByResidence counts tasks in a residence
func (r *TaskRepository) CountByResidence(residenceID uint) (int64, error) {
var count int64
diff --git a/internal/repositories/task_repo_test.go b/internal/repositories/task_repo_test.go
index 12432f7..e630afd 100644
--- a/internal/repositories/task_repo_test.go
+++ b/internal/repositories/task_repo_test.go
@@ -277,15 +277,6 @@ func TestTaskRepository_GetAllPriorities(t *testing.T) {
assert.Greater(t, len(priorities), 0)
}
-func TestTaskRepository_GetAllStatuses(t *testing.T) {
- db := testutil.SetupTestDB(t)
- repo := NewTaskRepository(db)
- testutil.SeedLookupData(t, db)
-
- statuses, err := repo.GetAllStatuses()
- require.NoError(t, err)
- assert.Greater(t, len(statuses), 0)
-}
func TestTaskRepository_GetAllFrequencies(t *testing.T) {
db := testutil.SetupTestDB(t)
@@ -396,16 +387,12 @@ func TestKanbanBoard_InProgressTasksGoToInProgressColumn(t *testing.T) {
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
- // Get "In Progress" status
- var inProgressStatus models.TaskStatus
- db.Where("name = ?", "In Progress").First(&inProgressStatus)
-
- // Create a task with "In Progress" status
+ // Create a task with InProgress = true
task := &models.Task{
ResidenceID: residence.ID,
CreatedByID: user.ID,
Title: "In Progress Task",
- StatusID: &inProgressStatus.ID,
+ InProgress: true,
}
err := db.Create(task).Error
require.NoError(t, err)
@@ -654,17 +641,13 @@ func TestKanbanBoard_CategoryPriority_CompletedTakesPrecedenceOverInProgress(t *
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
- // Get "In Progress" status
- var inProgressStatus models.TaskStatus
- db.Where("name = ?", "In Progress").First(&inProgressStatus)
-
- // Create a task that has "In Progress" status AND a completion
+ // Create a task that has InProgress = true AND a completion
// Completed should take precedence
task := &models.Task{
ResidenceID: residence.ID,
CreatedByID: user.ID,
Title: "In Progress with Completion",
- StatusID: &inProgressStatus.ID,
+ InProgress: true,
}
err := db.Create(task).Error
require.NoError(t, err)
diff --git a/internal/router/router.go b/internal/router/router.go
index 753edef..a698651 100644
--- a/internal/router/router.go
+++ b/internal/router/router.go
@@ -251,7 +251,6 @@ func setupPublicDataRoutes(api *gin.RouterGroup, residenceHandler *handlers.Resi
api.GET("/tasks/categories/", taskHandler.GetCategories)
api.GET("/tasks/priorities/", taskHandler.GetPriorities)
api.GET("/tasks/frequencies/", taskHandler.GetFrequencies)
- api.GET("/tasks/statuses/", taskHandler.GetStatuses)
api.GET("/contractors/specialties/", contractorHandler.GetSpecialties)
// Task template routes (public, for app autocomplete)
diff --git a/internal/services/cache_service.go b/internal/services/cache_service.go
index 83afb2f..cdb66f7 100644
--- a/internal/services/cache_service.go
+++ b/internal/services/cache_service.go
@@ -168,7 +168,6 @@ const (
LookupKeyPrefix = "lookup:"
LookupCategoriesKey = LookupKeyPrefix + "categories"
LookupPrioritiesKey = LookupKeyPrefix + "priorities"
- LookupStatusesKey = LookupKeyPrefix + "statuses"
LookupFrequenciesKey = LookupKeyPrefix + "frequencies"
LookupResidenceTypesKey = LookupKeyPrefix + "residence_types"
LookupSpecialtiesKey = LookupKeyPrefix + "specialties"
@@ -196,7 +195,6 @@ func (c *CacheService) InvalidateAllLookups(ctx context.Context) error {
keys := []string{
LookupCategoriesKey,
LookupPrioritiesKey,
- LookupStatusesKey,
LookupFrequenciesKey,
LookupResidenceTypesKey,
LookupSpecialtiesKey,
@@ -239,21 +237,6 @@ func (c *CacheService) InvalidatePriorities(ctx context.Context) error {
return c.Delete(ctx, LookupPrioritiesKey, StaticDataKey)
}
-// CacheStatuses caches task statuses
-func (c *CacheService) CacheStatuses(ctx context.Context, data interface{}) error {
- return c.CacheLookupData(ctx, LookupStatusesKey, data)
-}
-
-// GetCachedStatuses retrieves cached task statuses
-func (c *CacheService) GetCachedStatuses(ctx context.Context, dest interface{}) error {
- return c.GetCachedLookupData(ctx, LookupStatusesKey, dest)
-}
-
-// InvalidateStatuses removes cached task statuses
-func (c *CacheService) InvalidateStatuses(ctx context.Context) error {
- return c.Delete(ctx, LookupStatusesKey, StaticDataKey)
-}
-
// CacheFrequencies caches task frequencies
func (c *CacheService) CacheFrequencies(ctx context.Context, data interface{}) error {
return c.CacheLookupData(ctx, LookupFrequenciesKey, data)
diff --git a/internal/services/residence_service.go b/internal/services/residence_service.go
index f7469d1..5ef9978 100644
--- a/internal/services/residence_service.go
+++ b/internal/services/residence_service.go
@@ -616,8 +616,8 @@ func (s *ResidenceService) GenerateTasksReport(residenceID, userID uint) (*Tasks
if task.Priority != nil {
taskData.Priority = task.Priority.Name
}
- if task.Status != nil {
- taskData.Status = task.Status.Name
+ if task.InProgress {
+ taskData.Status = "In Progress"
}
// Use effective date for report (NextDueDate ?? DueDate)
effectiveDate := predicates.EffectiveDate(&task)
diff --git a/internal/services/residence_service_test.go b/internal/services/residence_service_test.go
index fcd184a..f7ee179 100644
--- a/internal/services/residence_service_test.go
+++ b/internal/services/residence_service_test.go
@@ -42,12 +42,12 @@ func TestResidenceService_CreateResidence(t *testing.T) {
resp, err := service.CreateResidence(req, user.ID)
require.NoError(t, err)
assert.NotNil(t, resp)
- assert.Equal(t, "Test House", resp.Name)
- assert.Equal(t, "123 Main St", resp.StreetAddress)
- assert.Equal(t, "Austin", resp.City)
- assert.Equal(t, "TX", resp.StateProvince)
- assert.Equal(t, "USA", resp.Country) // Default country
- assert.True(t, resp.IsPrimary) // Default is_primary
+ assert.Equal(t, "Test House", resp.Data.Name)
+ assert.Equal(t, "123 Main St", resp.Data.StreetAddress)
+ assert.Equal(t, "Austin", resp.Data.City)
+ assert.Equal(t, "TX", resp.Data.StateProvince)
+ assert.Equal(t, "USA", resp.Data.Country) // Default country
+ assert.True(t, resp.Data.IsPrimary) // Default is_primary
}
func TestResidenceService_CreateResidence_WithOptionalFields(t *testing.T) {
@@ -79,12 +79,12 @@ func TestResidenceService_CreateResidence_WithOptionalFields(t *testing.T) {
resp, err := service.CreateResidence(req, user.ID)
require.NoError(t, err)
- assert.Equal(t, "Canada", resp.Country)
- assert.Equal(t, 3, *resp.Bedrooms)
- assert.True(t, resp.Bathrooms.Equal(decimal.NewFromFloat(2.5)))
- assert.Equal(t, 2000, *resp.SquareFootage)
+ assert.Equal(t, "Canada", resp.Data.Country)
+ assert.Equal(t, 3, *resp.Data.Bedrooms)
+ assert.True(t, resp.Data.Bathrooms.Equal(decimal.NewFromFloat(2.5)))
+ assert.Equal(t, 2000, *resp.Data.SquareFootage)
// First residence defaults to primary regardless of request
- assert.True(t, resp.IsPrimary)
+ assert.True(t, resp.Data.IsPrimary)
}
func TestResidenceService_GetResidence(t *testing.T) {
@@ -166,8 +166,8 @@ func TestResidenceService_UpdateResidence(t *testing.T) {
resp, err := service.UpdateResidence(residence.ID, user.ID, req)
require.NoError(t, err)
- assert.Equal(t, "Updated Name", resp.Name)
- assert.Equal(t, "Dallas", resp.City)
+ assert.Equal(t, "Updated Name", resp.Data.Name)
+ assert.Equal(t, "Dallas", resp.Data.City)
}
func TestResidenceService_UpdateResidence_NotOwner(t *testing.T) {
@@ -201,7 +201,7 @@ func TestResidenceService_DeleteResidence(t *testing.T) {
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
- err := service.DeleteResidence(residence.ID, user.ID)
+ _, err := service.DeleteResidence(residence.ID, user.ID)
require.NoError(t, err)
// Should not be found
@@ -221,7 +221,7 @@ func TestResidenceService_DeleteResidence_NotOwner(t *testing.T) {
residence := testutil.CreateTestResidence(t, db, owner.ID, "Test House")
residenceRepo.AddUser(residence.ID, sharedUser.ID)
- err := service.DeleteResidence(residence.ID, sharedUser.ID)
+ _, err := service.DeleteResidence(residence.ID, sharedUser.ID)
assert.ErrorIs(t, err, ErrNotResidenceOwner)
}
diff --git a/internal/services/task_categorization_test.go b/internal/services/task_categorization_test.go
index 2f35563..0276989 100644
--- a/internal/services/task_categorization_test.go
+++ b/internal/services/task_categorization_test.go
@@ -33,7 +33,7 @@ KANBAN COLUMNS (in priority order):
----------------------------------
1. CANCELLED: Task.IsCancelled = true
2. COMPLETED: NextDueDate = nil AND has completions (one-time task done)
-3. IN_PROGRESS: Status.Name = "In Progress"
+3. IN_PROGRESS: InProgress = true
4. OVERDUE: NextDueDate < now
5. DUE_SOON: NextDueDate < now + daysThreshold (default 30)
6. UPCOMING: Everything else (NextDueDate >= threshold or no due date)
@@ -72,6 +72,14 @@ func daysAgo(n int) time.Time {
return time.Now().UTC().AddDate(0, 0, -n)
}
+// isTaskCompleted checks if a task is permanently completed (one-time task done).
+// A task is completed when it has completions AND NextDueDate is nil.
+func isTaskCompleted(task *models.Task) bool {
+ if len(task.Completions) == 0 {
+ return false
+ }
+ return task.NextDueDate == nil
+}
// ============================================================================
// isTaskCompleted FUNCTION TESTS
@@ -157,7 +165,7 @@ func TestGetButtonTypesForTask_CompletedOneTimeTask(t *testing.T) {
func TestGetButtonTypesForTask_InProgressTask(t *testing.T) {
task := &models.Task{
NextDueDate: ptr(daysFromNow(10)),
- Status: &models.TaskStatus{Name: "In Progress"},
+ InProgress: true,
}
buttons := GetButtonTypesForTask(task, 30)
@@ -237,7 +245,7 @@ func TestGetIOSCategoryForTask_CompletedTask(t *testing.T) {
func TestGetIOSCategoryForTask_InProgressTask(t *testing.T) {
task := &models.Task{
NextDueDate: ptr(daysFromNow(10)),
- Status: &models.TaskStatus{Name: "In Progress"},
+ InProgress: true,
}
category := GetIOSCategoryForTask(task)
@@ -285,7 +293,7 @@ func TestDetermineKanbanColumn_CompletedOneTimeTask(t *testing.T) {
func TestDetermineKanbanColumn_InProgressTask(t *testing.T) {
task := &models.Task{
NextDueDate: ptr(daysAgo(5)), // Even overdue
- Status: &models.TaskStatus{Name: "In Progress"},
+ InProgress: true,
}
column := responses.DetermineKanbanColumn(task, 30)
@@ -902,7 +910,7 @@ func TestEdgeCase_CancelledAndOverdue(t *testing.T) {
func TestEdgeCase_InProgressAndOverdue(t *testing.T) {
task := &models.Task{
NextDueDate: ptr(daysAgo(5)),
- Status: &models.TaskStatus{Name: "In Progress"},
+ InProgress: true,
}
column := responses.DetermineKanbanColumn(task, 30)
@@ -1011,7 +1019,7 @@ func TestButtonTypes_ConsistencyWithKanbanColumn(t *testing.T) {
name: "In Progress task",
task: &models.Task{
NextDueDate: ptr(daysFromNow(10)),
- Status: &models.TaskStatus{Name: "In Progress"},
+ InProgress: true,
},
expectedColumn: "in_progress_tasks",
expectedButtons: []string{"edit", "complete", "cancel"},
@@ -1062,7 +1070,7 @@ func TestPriorityOrder_CancelledBeatsEverything(t *testing.T) {
task := &models.Task{
IsCancelled: true,
NextDueDate: ptr(daysAgo(10)),
- Status: &models.TaskStatus{Name: "In Progress"},
+ InProgress: true,
Completions: []models.TaskCompletion{{CompletedAt: daysAgo(1)}},
}
@@ -1074,7 +1082,7 @@ func TestPriorityOrder_CompletedBeatsInProgress(t *testing.T) {
// One-time task with In Progress status but completed
task := &models.Task{
NextDueDate: nil,
- Status: &models.TaskStatus{Name: "In Progress"},
+ InProgress: true,
Completions: []models.TaskCompletion{{CompletedAt: daysAgo(1)}},
}
@@ -1086,7 +1094,7 @@ func TestPriorityOrder_InProgressBeatsDateBased(t *testing.T) {
// Overdue task that's in progress
task := &models.Task{
NextDueDate: ptr(daysAgo(10)),
- Status: &models.TaskStatus{Name: "In Progress"},
+ InProgress: true,
}
column := responses.DetermineKanbanColumn(task, 30)
diff --git a/internal/services/task_service.go b/internal/services/task_service.go
index 0ca2053..48cb031 100644
--- a/internal/services/task_service.go
+++ b/internal/services/task_service.go
@@ -173,8 +173,8 @@ func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint) (
Description: req.Description,
CategoryID: req.CategoryID,
PriorityID: req.PriorityID,
- StatusID: req.StatusID,
FrequencyID: req.FrequencyID,
+ InProgress: req.InProgress,
AssignedToID: req.AssignedToID,
DueDate: dueDate,
NextDueDate: dueDate, // Initialize next_due_date to due_date
@@ -230,12 +230,12 @@ func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRe
if req.PriorityID != nil {
task.PriorityID = req.PriorityID
}
- if req.StatusID != nil {
- task.StatusID = req.StatusID
- }
if req.FrequencyID != nil {
task.FrequencyID = req.FrequencyID
}
+ if req.InProgress != nil {
+ task.InProgress = *req.InProgress
+ }
if req.AssignedToID != nil {
task.AssignedToID = req.AssignedToID
}
@@ -324,13 +324,7 @@ func (s *TaskService) MarkInProgress(taskID, userID uint) (*responses.TaskWithSu
return nil, ErrTaskAccessDenied
}
- // Find "In Progress" status
- status, err := s.taskRepo.FindStatusByName("In Progress")
- if err != nil {
- return nil, err
- }
-
- if err := s.taskRepo.MarkInProgress(taskID, status.ID); err != nil {
+ if err := s.taskRepo.MarkInProgress(taskID); err != nil {
return nil, err
}
@@ -534,24 +528,22 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest
return nil, err
}
- // Update next_due_date and status based on frequency
- // - If frequency is "Once" (days = nil or 0), set next_due_date to nil and status to "Completed"
+ // Update next_due_date and in_progress based on frequency
+ // - If frequency is "Once" (days = nil or 0), set next_due_date to nil (marks as completed)
// - If frequency is recurring, calculate next_due_date = completion_date + frequency_days
- // and reset status to "Pending" so task shows in correct kanban column
+ // and reset in_progress to false so task shows in correct kanban column
if task.Frequency == nil || task.Frequency.Days == nil || *task.Frequency.Days == 0 {
- // One-time task - clear next_due_date and set status to "Completed" (ID=3)
+ // One-time task - clear next_due_date (completion is determined by NextDueDate == nil + has completions)
task.NextDueDate = nil
- completedStatusID := uint(3)
- task.StatusID = &completedStatusID
+ task.InProgress = false
} else {
// Recurring task - calculate next due date from completion date + frequency
nextDue := completedAt.AddDate(0, 0, *task.Frequency.Days)
task.NextDueDate = &nextDue
- // Reset status to "Pending" (ID=1) so task appears in upcoming/due_soon
+ // Reset in_progress to false so task appears in upcoming/due_soon
// instead of staying in "In Progress" column
- pendingStatusID := uint(1)
- task.StatusID = &pendingStatusID
+ task.InProgress = false
}
if err := s.taskRepo.Update(task); err != nil {
log.Error().Err(err).Uint("task_id", task.ID).Msg("Failed to update task after completion")
@@ -633,20 +625,18 @@ func (s *TaskService) QuickComplete(taskID uint, userID uint) error {
return err
}
- // Update next_due_date and status based on frequency
+ // Update next_due_date and in_progress based on frequency
if task.Frequency == nil || task.Frequency.Days == nil || *task.Frequency.Days == 0 {
- // One-time task - clear next_due_date and set status to "Completed" (ID=3)
+ // One-time task - clear next_due_date (completion is determined by NextDueDate == nil + has completions)
task.NextDueDate = nil
- completedStatusID := uint(3)
- task.StatusID = &completedStatusID
+ task.InProgress = false
} else {
// Recurring task - calculate next due date from completion date + frequency
nextDue := completedAt.AddDate(0, 0, *task.Frequency.Days)
task.NextDueDate = &nextDue
- // Reset status to "Pending" (ID=1)
- pendingStatusID := uint(1)
- task.StatusID = &pendingStatusID
+ // Reset in_progress to false
+ task.InProgress = false
}
if err := s.taskRepo.Update(task); err != nil {
log.Error().Err(err).Uint("task_id", task.ID).Msg("Failed to update task after quick completion")
@@ -858,20 +848,6 @@ func (s *TaskService) GetPriorities() ([]responses.TaskPriorityResponse, error)
return result, nil
}
-// GetStatuses returns all task statuses
-func (s *TaskService) GetStatuses() ([]responses.TaskStatusResponse, error) {
- statuses, err := s.taskRepo.GetAllStatuses()
- if err != nil {
- return nil, err
- }
-
- result := make([]responses.TaskStatusResponse, len(statuses))
- for i, st := range statuses {
- result[i] = *responses.NewTaskStatusResponse(&st)
- }
- return result, nil
-}
-
// GetFrequencies returns all task frequencies
func (s *TaskService) GetFrequencies() ([]responses.TaskFrequencyResponse, error) {
frequencies, err := s.taskRepo.GetAllFrequencies()
diff --git a/internal/services/task_service_test.go b/internal/services/task_service_test.go
index a81288d..b358c2e 100644
--- a/internal/services/task_service_test.go
+++ b/internal/services/task_service_test.go
@@ -41,9 +41,9 @@ func TestTaskService_CreateTask(t *testing.T) {
resp, err := service.CreateTask(req, user.ID)
require.NoError(t, err)
- assert.NotZero(t, resp.ID)
- assert.Equal(t, "Fix leaky faucet", resp.Title)
- assert.Equal(t, "Kitchen faucet is dripping", resp.Description)
+ assert.NotZero(t, resp.Data.ID)
+ assert.Equal(t, "Fix leaky faucet", resp.Data.Title)
+ assert.Equal(t, "Kitchen faucet is dripping", resp.Data.Description)
}
func TestTaskService_CreateTask_WithOptionalFields(t *testing.T) {
@@ -76,10 +76,10 @@ func TestTaskService_CreateTask_WithOptionalFields(t *testing.T) {
resp, err := service.CreateTask(req, user.ID)
require.NoError(t, err)
- assert.NotNil(t, resp.Category)
- assert.NotNil(t, resp.Priority)
- assert.NotNil(t, resp.DueDate)
- assert.NotNil(t, resp.EstimatedCost)
+ assert.NotNil(t, resp.Data.Category)
+ assert.NotNil(t, resp.Data.Priority)
+ assert.NotNil(t, resp.Data.DueDate)
+ assert.NotNil(t, resp.Data.EstimatedCost)
}
func TestTaskService_CreateTask_AccessDenied(t *testing.T) {
@@ -180,8 +180,8 @@ func TestTaskService_UpdateTask(t *testing.T) {
resp, err := service.UpdateTask(task.ID, user.ID, req)
require.NoError(t, err)
- assert.Equal(t, "Updated Title", resp.Title)
- assert.Equal(t, "Updated description", resp.Description)
+ assert.Equal(t, "Updated Title", resp.Data.Title)
+ assert.Equal(t, "Updated description", resp.Data.Description)
}
func TestTaskService_DeleteTask(t *testing.T) {
@@ -195,7 +195,7 @@ func TestTaskService_DeleteTask(t *testing.T) {
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Test Task")
- err := service.DeleteTask(task.ID, user.ID)
+ _, err := service.DeleteTask(task.ID, user.ID)
require.NoError(t, err)
_, err = service.GetTask(task.ID, user.ID)
@@ -215,7 +215,7 @@ func TestTaskService_CancelTask(t *testing.T) {
resp, err := service.CancelTask(task.ID, user.ID)
require.NoError(t, err)
- assert.True(t, resp.IsCancelled)
+ assert.True(t, resp.Data.IsCancelled)
}
func TestTaskService_CancelTask_AlreadyCancelled(t *testing.T) {
@@ -248,7 +248,7 @@ func TestTaskService_UncancelTask(t *testing.T) {
service.CancelTask(task.ID, user.ID)
resp, err := service.UncancelTask(task.ID, user.ID)
require.NoError(t, err)
- assert.False(t, resp.IsCancelled)
+ assert.False(t, resp.Data.IsCancelled)
}
func TestTaskService_ArchiveTask(t *testing.T) {
@@ -264,7 +264,7 @@ func TestTaskService_ArchiveTask(t *testing.T) {
resp, err := service.ArchiveTask(task.ID, user.ID)
require.NoError(t, err)
- assert.True(t, resp.IsArchived)
+ assert.True(t, resp.Data.IsArchived)
}
func TestTaskService_UnarchiveTask(t *testing.T) {
@@ -281,7 +281,7 @@ func TestTaskService_UnarchiveTask(t *testing.T) {
service.ArchiveTask(task.ID, user.ID)
resp, err := service.UnarchiveTask(task.ID, user.ID)
require.NoError(t, err)
- assert.False(t, resp.IsArchived)
+ assert.False(t, resp.Data.IsArchived)
}
func TestTaskService_MarkInProgress(t *testing.T) {
@@ -297,8 +297,7 @@ func TestTaskService_MarkInProgress(t *testing.T) {
resp, err := service.MarkInProgress(task.ID, user.ID)
require.NoError(t, err)
- assert.NotNil(t, resp.Status)
- assert.Equal(t, "In Progress", resp.Status.Name)
+ assert.True(t, resp.Data.InProgress)
}
func TestTaskService_CreateCompletion(t *testing.T) {
@@ -319,12 +318,12 @@ func TestTaskService_CreateCompletion(t *testing.T) {
resp, err := service.CreateCompletion(req, user.ID)
require.NoError(t, err)
- assert.NotZero(t, resp.ID)
- assert.Equal(t, task.ID, resp.TaskID)
- assert.Equal(t, "Completed successfully", resp.Notes)
+ assert.NotZero(t, resp.Data.ID)
+ assert.Equal(t, task.ID, resp.Data.TaskID)
+ assert.Equal(t, "Completed successfully", resp.Data.Notes)
}
-func TestTaskService_CreateCompletion_RecurringTask_ResetsStatusToPending(t *testing.T) {
+func TestTaskService_CreateCompletion_RecurringTask_ResetsInProgress(t *testing.T) {
db := testutil.SetupTestDB(t)
testutil.SeedLookupData(t, db)
taskRepo := repositories.NewTaskRepository(db)
@@ -334,20 +333,16 @@ func TestTaskService_CreateCompletion_RecurringTask_ResetsStatusToPending(t *tes
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
- // Get the "In Progress" status (ID=2) and a recurring frequency
- var inProgressStatus models.TaskStatus
- db.Where("name = ?", "In Progress").First(&inProgressStatus)
-
var monthlyFrequency models.TaskFrequency
db.Where("name = ?", "Monthly").First(&monthlyFrequency)
- // Create a recurring task with "In Progress" status
+ // Create a recurring task that is in progress
dueDate := time.Now().AddDate(0, 0, 7) // Due in 7 days
task := &models.Task{
ResidenceID: residence.ID,
CreatedByID: user.ID,
Title: "Recurring Task",
- StatusID: &inProgressStatus.ID,
+ InProgress: true,
FrequencyID: &monthlyFrequency.ID,
DueDate: &dueDate,
NextDueDate: &dueDate,
@@ -365,24 +360,21 @@ func TestTaskService_CreateCompletion_RecurringTask_ResetsStatusToPending(t *tes
resp, err := service.CreateCompletion(req, user.ID)
require.NoError(t, err)
- assert.NotZero(t, resp.ID)
+ assert.NotZero(t, resp.Data.ID)
- // Verify the task in the response has status reset to "Pending" (ID=1)
- require.NotNil(t, resp.Task, "Response should include the updated task")
- require.NotNil(t, resp.Task.StatusID, "Task should have a status ID")
- assert.Equal(t, uint(1), *resp.Task.StatusID, "Recurring task status should be reset to Pending (ID=1) after completion")
+ // Verify the task in the response has InProgress reset to false
+ require.NotNil(t, resp.Data.Task, "Response should include the updated task")
+ assert.False(t, resp.Data.Task.InProgress, "Recurring task InProgress should be reset to false after completion")
// Verify NextDueDate was updated (should be ~30 days from now for monthly)
- require.NotNil(t, resp.Task.NextDueDate, "Recurring task should have NextDueDate set")
+ require.NotNil(t, resp.Data.Task.NextDueDate, "Recurring task should have NextDueDate set")
expectedNextDue := time.Now().AddDate(0, 0, 30) // Monthly = 30 days
- assert.WithinDuration(t, expectedNextDue, *resp.Task.NextDueDate, 24*time.Hour, "NextDueDate should be approximately 30 days from now")
+ assert.WithinDuration(t, expectedNextDue, *resp.Data.Task.NextDueDate, 24*time.Hour, "NextDueDate should be approximately 30 days from now")
// Also verify by reloading from database directly
var reloadedTask models.Task
- db.Preload("Status").First(&reloadedTask, task.ID)
- require.NotNil(t, reloadedTask.StatusID)
- assert.Equal(t, uint(1), *reloadedTask.StatusID, "Database should show Pending status")
- assert.Equal(t, "Pending", reloadedTask.Status.Name)
+ db.First(&reloadedTask, task.ID)
+ assert.False(t, reloadedTask.InProgress, "Database should show InProgress=false")
}
func TestTaskService_GetCompletion(t *testing.T) {
@@ -428,7 +420,7 @@ func TestTaskService_DeleteCompletion(t *testing.T) {
}
db.Create(completion)
- err := service.DeleteCompletion(completion.ID, user.ID)
+ _, err := service.DeleteCompletion(completion.ID, user.ID)
require.NoError(t, err)
_, err = service.GetCompletion(completion.ID, user.ID)
@@ -470,18 +462,6 @@ func TestTaskService_GetPriorities(t *testing.T) {
}
}
-func TestTaskService_GetStatuses(t *testing.T) {
- db := testutil.SetupTestDB(t)
- testutil.SeedLookupData(t, db)
- taskRepo := repositories.NewTaskRepository(db)
- residenceRepo := repositories.NewResidenceRepository(db)
- service := NewTaskService(taskRepo, residenceRepo)
-
- statuses, err := service.GetStatuses()
- require.NoError(t, err)
- assert.Greater(t, len(statuses), 0)
-}
-
func TestTaskService_GetFrequencies(t *testing.T) {
db := testutil.SetupTestDB(t)
testutil.SeedLookupData(t, db)
diff --git a/internal/task/categorization/chain_test.go b/internal/task/categorization/chain_test.go
index 2aaa5b6..b460832 100644
--- a/internal/task/categorization/chain_test.go
+++ b/internal/task/categorization/chain_test.go
@@ -18,7 +18,6 @@ func TestCategorizeTask_PriorityOrder(t *testing.T) {
yesterday := now.AddDate(0, 0, -1)
in5Days := now.AddDate(0, 0, 5)
in60Days := now.AddDate(0, 0, 60)
- inProgressStatus := &models.TaskStatus{Name: "In Progress"}
daysThreshold := 30
tests := []struct {
@@ -32,7 +31,7 @@ func TestCategorizeTask_PriorityOrder(t *testing.T) {
task: &models.Task{
IsCancelled: true,
NextDueDate: timePtr(yesterday), // Would be overdue
- Status: inProgressStatus, // Would be in progress
+ InProgress: true, // Would be in progress
Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}}, // Would be completed if NextDueDate was nil
},
expected: categorization.ColumnCancelled,
@@ -68,7 +67,7 @@ func TestCategorizeTask_PriorityOrder(t *testing.T) {
IsCancelled: false,
IsArchived: false,
NextDueDate: timePtr(yesterday), // Would be overdue
- Status: inProgressStatus,
+ InProgress: true,
Completions: []models.TaskCompletion{},
},
expected: categorization.ColumnInProgress,
@@ -151,13 +150,13 @@ func TestCategorizeTasksIntoColumns(t *testing.T) {
daysThreshold := 30
tasks := []models.Task{
- {BaseModel: models.BaseModel{ID: 1}, IsCancelled: true}, // Cancelled
- {BaseModel: models.BaseModel{ID: 2}, NextDueDate: nil, Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}}}, // Completed
- {BaseModel: models.BaseModel{ID: 3}, Status: &models.TaskStatus{Name: "In Progress"}}, // In Progress
- {BaseModel: models.BaseModel{ID: 4}, NextDueDate: timePtr(yesterday)}, // Overdue
- {BaseModel: models.BaseModel{ID: 5}, NextDueDate: timePtr(in5Days)}, // Due Soon
- {BaseModel: models.BaseModel{ID: 6}, NextDueDate: timePtr(in60Days)}, // Upcoming
- {BaseModel: models.BaseModel{ID: 7}}, // Upcoming (no due date)
+ {BaseModel: models.BaseModel{ID: 1}, IsCancelled: true}, // Cancelled
+ {BaseModel: models.BaseModel{ID: 2}, NextDueDate: nil, Completions: []models.TaskCompletion{{BaseModel: models.BaseModel{ID: 1}}}}, // Completed
+ {BaseModel: models.BaseModel{ID: 3}, InProgress: true}, // In Progress
+ {BaseModel: models.BaseModel{ID: 4}, NextDueDate: timePtr(yesterday)}, // Overdue
+ {BaseModel: models.BaseModel{ID: 5}, NextDueDate: timePtr(in5Days)}, // Due Soon
+ {BaseModel: models.BaseModel{ID: 6}, NextDueDate: timePtr(in60Days)}, // Upcoming
+ {BaseModel: models.BaseModel{ID: 7}}, // Upcoming (no due date)
}
result := categorization.CategorizeTasksIntoColumns(tasks, daysThreshold)
diff --git a/internal/task/consistency_test.go b/internal/task/consistency_test.go
index 88b94ea..ff3252d 100644
--- a/internal/task/consistency_test.go
+++ b/internal/task/consistency_test.go
@@ -103,15 +103,6 @@ func createCompletion(t *testing.T, taskID uint) {
}
}
-// getInProgressStatusID returns the ID of the "In Progress" status
-func getInProgressStatusID(t *testing.T) *uint {
- var status models.TaskStatus
- if err := testDB.Where("name = ?", "In Progress").First(&status).Error; err != nil {
- t.Logf("In Progress status not found, skipping in-progress tests")
- return nil
- }
- return &status.ID
-}
// TaskTestCase defines a test scenario with expected categorization
type TaskTestCase struct {
@@ -147,8 +138,6 @@ func TestAllThreeLayersMatch(t *testing.T) {
in60Days := now.AddDate(0, 0, 60)
daysThreshold := 30
- inProgressStatusID := getInProgressStatusID(t)
-
// Define all test cases with expected results for each layer
testCases := []TaskTestCase{
{
@@ -293,27 +282,23 @@ func TestAllThreeLayersMatch(t *testing.T) {
ExpectDueSoon: false,
ExpectUpcoming: false,
},
- }
-
- // Add in-progress test case only if status exists
- if inProgressStatusID != nil {
- testCases = append(testCases, TaskTestCase{
+ {
Name: "in_progress_overdue",
Task: &models.Task{
Title: "in_progress_overdue",
NextDueDate: timePtr(yesterday), // Would be overdue
- StatusID: inProgressStatusID,
+ InProgress: true,
IsCancelled: false,
IsArchived: false,
},
ExpectedColumn: categorization.ColumnInProgress, // In Progress takes priority
ExpectCompleted: false,
ExpectActive: true,
- ExpectOverdue: true, // Predicate says overdue (doesn't check status)
+ ExpectOverdue: true, // Predicate says overdue (doesn't check InProgress)
ExpectDueSoon: false,
ExpectUpcoming: false,
ExpectInProgress: true,
- })
+ },
}
// Create all tasks in database
@@ -330,7 +315,6 @@ func TestAllThreeLayersMatch(t *testing.T) {
var allTasks []models.Task
err := testDB.
Preload("Completions").
- Preload("Status").
Where("residence_id = ?", residenceID).
Find(&allTasks).Error
if err != nil {
@@ -490,26 +474,24 @@ func TestAllThreeLayersMatch(t *testing.T) {
}
})
- // Test ScopeInProgress (if status exists)
- if inProgressStatusID != nil {
- t.Run("ScopeInProgress", func(t *testing.T) {
- var scopeResults []models.Task
- testDB.Model(&models.Task{}).
- Scopes(scopes.ScopeForResidence(residenceID), scopes.ScopeInProgress).
- Find(&scopeResults)
+ // Test ScopeInProgress
+ t.Run("ScopeInProgress", func(t *testing.T) {
+ var scopeResults []models.Task
+ testDB.Model(&models.Task{}).
+ Scopes(scopes.ScopeForResidence(residenceID), scopes.ScopeInProgress).
+ Find(&scopeResults)
- predicateCount := 0
- for _, task := range allTasks {
- if predicates.IsInProgress(&task) {
- predicateCount++
- }
+ predicateCount := 0
+ for _, task := range allTasks {
+ if predicates.IsInProgress(&task) {
+ predicateCount++
}
+ }
- if len(scopeResults) != predicateCount {
- t.Errorf("ScopeInProgress returned %d, predicates found %d", len(scopeResults), predicateCount)
- }
- })
- }
+ if len(scopeResults) != predicateCount {
+ t.Errorf("ScopeInProgress returned %d, predicates found %d", len(scopeResults), predicateCount)
+ }
+ })
})
// ========== TEST CATEGORIZATION MATCHES SCOPES FOR KANBAN ==========
@@ -527,7 +509,6 @@ func TestAllThreeLayersMatch(t *testing.T) {
t.Run("overdue_column", func(t *testing.T) {
var scopeResults []models.Task
testDB.Model(&models.Task{}).
- Preload("Status").
Scopes(scopes.ScopeForResidence(residenceID), scopes.ScopeOverdue(now)).
Find(&scopeResults)
@@ -612,7 +593,7 @@ func TestSameDayOverdueConsistency(t *testing.T) {
// Reload with preloads
var loadedTask models.Task
- testDB.Preload("Completions").Preload("Status").First(&loadedTask, task.ID)
+ testDB.Preload("Completions").First(&loadedTask, task.ID)
// All three layers should agree
predicateResult := predicates.IsOverdue(&loadedTask, now)
diff --git a/internal/task/predicates/predicates.go b/internal/task/predicates/predicates.go
index e81f10a..157ee1a 100644
--- a/internal/task/predicates/predicates.go
+++ b/internal/task/predicates/predicates.go
@@ -60,13 +60,13 @@ func IsArchived(task *models.Task) bool {
return task.IsArchived
}
-// IsInProgress returns true if the task has status "In Progress".
+// IsInProgress returns true if the task is marked as in progress.
//
// SQL equivalent (in scopes.go ScopeInProgress):
//
-// task_taskstatus.name = 'In Progress'
+// in_progress = true
func IsInProgress(task *models.Task) bool {
- return task.Status != nil && task.Status.Name == "In Progress"
+ return task.InProgress
}
// =============================================================================
diff --git a/internal/task/predicates/predicates_test.go b/internal/task/predicates/predicates_test.go
index 24e7411..dbb8376 100644
--- a/internal/task/predicates/predicates_test.go
+++ b/internal/task/predicates/predicates_test.go
@@ -102,27 +102,19 @@ func TestIsActive(t *testing.T) {
}
func TestIsInProgress(t *testing.T) {
- inProgressStatus := &models.TaskStatus{Name: "In Progress"}
- pendingStatus := &models.TaskStatus{Name: "Pending"}
-
tests := []struct {
name string
task *models.Task
expected bool
}{
{
- name: "in progress: status is In Progress",
- task: &models.Task{Status: inProgressStatus},
+ name: "in progress: InProgress is true",
+ task: &models.Task{InProgress: true},
expected: true,
},
{
- name: "not in progress: status is Pending",
- task: &models.Task{Status: pendingStatus},
- expected: false,
- },
- {
- name: "not in progress: no status",
- task: &models.Task{Status: nil},
+ name: "not in progress: InProgress is false",
+ task: &models.Task{InProgress: false},
expected: false,
},
}
diff --git a/internal/task/scopes/scopes.go b/internal/task/scopes/scopes.go
index c50037d..36cef6c 100644
--- a/internal/task/scopes/scopes.go
+++ b/internal/task/scopes/scopes.go
@@ -73,22 +73,22 @@ func ScopeNotCompleted(db *gorm.DB) *gorm.DB {
)
}
-// ScopeInProgress filters to tasks with status "In Progress".
+// ScopeInProgress filters to tasks marked as in progress.
//
// Predicate equivalent: IsInProgress(task)
//
-// SQL: Joins task_taskstatus and filters by name = 'In Progress'
+// SQL: in_progress = true
func ScopeInProgress(db *gorm.DB) *gorm.DB {
- return db.Joins("LEFT JOIN task_taskstatus ON task_taskstatus.id = task_task.status_id").
- Where("task_taskstatus.name = ?", "In Progress")
+ return db.Where("in_progress = ?", true)
}
-// ScopeNotInProgress excludes tasks with status "In Progress".
+// ScopeNotInProgress excludes tasks marked as in progress.
//
// Predicate equivalent: !IsInProgress(task)
+//
+// SQL: in_progress = false
func ScopeNotInProgress(db *gorm.DB) *gorm.DB {
- return db.Joins("LEFT JOIN task_taskstatus ON task_taskstatus.id = task_task.status_id").
- Where("task_taskstatus.name != ? OR task_taskstatus.name IS NULL", "In Progress")
+ return db.Where("in_progress = ?", false)
}
// =============================================================================
diff --git a/internal/task/scopes/scopes_test.go b/internal/task/scopes/scopes_test.go
index 3d6586e..1bccc30 100644
--- a/internal/task/scopes/scopes_test.go
+++ b/internal/task/scopes/scopes_test.go
@@ -54,7 +54,6 @@ func TestMain(m *testing.M) {
err = testDB.AutoMigrate(
&models.Task{},
&models.TaskCompletion{},
- &models.TaskStatus{},
&models.Residence{},
)
if err != nil {
@@ -77,7 +76,6 @@ func cleanupTestData() {
}
testDB.Exec("DELETE FROM task_taskcompletion WHERE task_id IN (SELECT id FROM task_task WHERE title LIKE 'test_%')")
testDB.Exec("DELETE FROM task_task WHERE title LIKE 'test_%'")
- testDB.Exec("DELETE FROM task_taskstatus WHERE name LIKE 'test_%'")
testDB.Exec("DELETE FROM residence_residence WHERE name LIKE 'test_%'")
}
@@ -102,16 +100,6 @@ func createTestResidence(t *testing.T) uint {
return residence.ID
}
-// createTestStatus creates a test status and returns it
-func createTestStatus(t *testing.T, name string) *models.TaskStatus {
- status := &models.TaskStatus{
- Name: "test_" + name,
- }
- if err := testDB.Create(status).Error; err != nil {
- t.Fatalf("Failed to create test status: %v", err)
- }
- return status
-}
// createTestTask creates a task with the given properties
func createTestTask(t *testing.T, residenceID uint, task *models.Task) *models.Task {
@@ -587,41 +575,18 @@ func TestScopeInProgressMatchesPredicate(t *testing.T) {
}
residenceID := createTestResidence(t)
-
- // For InProgress, we need to use the exact status name "In Progress" because
- // the scope joins on task_taskstatus.name = 'In Progress'
- // First, try to find existing "In Progress" status, or create one
- var inProgressStatus models.TaskStatus
- if err := testDB.Where("name = ?", "In Progress").First(&inProgressStatus).Error; err != nil {
- // Create it if it doesn't exist
- inProgressStatus = models.TaskStatus{Name: "In Progress"}
- testDB.Create(&inProgressStatus)
- }
-
- var pendingStatus models.TaskStatus
- if err := testDB.Where("name = ?", "Pending").First(&pendingStatus).Error; err != nil {
- pendingStatus = models.TaskStatus{Name: "Pending"}
- testDB.Create(&pendingStatus)
- }
-
defer cleanupTestData()
// In progress task
createTestTask(t, residenceID, &models.Task{
- Title: "in_progress",
- StatusID: &inProgressStatus.ID,
+ Title: "in_progress",
+ InProgress: true,
})
- // Not in progress: different status
+ // Not in progress: InProgress is false
createTestTask(t, residenceID, &models.Task{
- Title: "pending",
- StatusID: &pendingStatus.ID,
- })
-
- // Not in progress: no status
- createTestTask(t, residenceID, &models.Task{
- Title: "no_status",
- StatusID: nil,
+ Title: "not_in_progress",
+ InProgress: false,
})
// Query using scope
@@ -633,9 +598,9 @@ func TestScopeInProgressMatchesPredicate(t *testing.T) {
t.Fatalf("Scope query failed: %v", err)
}
- // Query all tasks with status preloaded and filter with predicate
+ // Query all tasks and filter with predicate
var allTasks []models.Task
- testDB.Preload("Status").Where("residence_id = ?", residenceID).Find(&allTasks)
+ testDB.Where("residence_id = ?", residenceID).Find(&allTasks)
var predicateResults []models.Task
for _, task := range allTasks {
diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go
index b73ea7f..7d21289 100644
--- a/internal/testutil/testutil.go
+++ b/internal/testutil/testutil.go
@@ -46,7 +46,6 @@ func SetupTestDB(t *testing.T) *gorm.DB {
&models.Task{},
&models.TaskCategory{},
&models.TaskPriority{},
- &models.TaskStatus{},
&models.TaskFrequency{},
&models.TaskCompletion{},
&models.TaskCompletionImage{},
@@ -184,17 +183,6 @@ func CreateTestTaskPriority(t *testing.T, db *gorm.DB, name string, level int) *
return priority
}
-// CreateTestTaskStatus creates a test task status
-func CreateTestTaskStatus(t *testing.T, db *gorm.DB, name string) *models.TaskStatus {
- status := &models.TaskStatus{
- Name: name,
- DisplayOrder: 1,
- }
- err := db.Create(status).Error
- require.NoError(t, err)
- return status
-}
-
// CreateTestTaskFrequency creates a test task frequency
func CreateTestTaskFrequency(t *testing.T, db *gorm.DB, name string, days *int) *models.TaskFrequency {
freq := &models.TaskFrequency{
@@ -256,17 +244,6 @@ func SeedLookupData(t *testing.T, db *gorm.DB) {
db.Create(&p)
}
- // Task statuses
- statuses := []models.TaskStatus{
- {Name: "Pending", DisplayOrder: 1},
- {Name: "In Progress", DisplayOrder: 2},
- {Name: "Completed", DisplayOrder: 3},
- {Name: "Cancelled", DisplayOrder: 4},
- }
- for _, s := range statuses {
- db.Create(&s)
- }
-
// Task frequencies
days7 := 7
days30 := 30
diff --git a/migrations/005_replace_status_with_in_progress.down.sql b/migrations/005_replace_status_with_in_progress.down.sql
new file mode 100644
index 0000000..eccb631
--- /dev/null
+++ b/migrations/005_replace_status_with_in_progress.down.sql
@@ -0,0 +1,45 @@
+-- Rollback: Restore status_id foreign key from in_progress boolean
+
+-- Step 1: Recreate the task_taskstatus table
+CREATE TABLE IF NOT EXISTS task_taskstatus (
+ id SERIAL PRIMARY KEY,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ deleted_at TIMESTAMPTZ,
+ name VARCHAR(20) NOT NULL,
+ description TEXT,
+ color VARCHAR(7),
+ display_order INTEGER NOT NULL DEFAULT 0
+);
+
+-- Step 2: Seed the status lookup data
+INSERT INTO task_taskstatus (name, description, color, display_order) VALUES
+ ('Pending', 'Task is waiting to be started', '#808080', 1),
+ ('In Progress', 'Task is currently being worked on', '#3498db', 2),
+ ('Completed', 'Task has been finished', '#27ae60', 3),
+ ('On Hold', 'Task is temporarily paused', '#f39c12', 4),
+ ('Cancelled', 'Task has been cancelled', '#e74c3c', 5)
+ON CONFLICT DO NOTHING;
+
+-- Step 3: Add status_id column back
+ALTER TABLE task_task ADD COLUMN IF NOT EXISTS status_id INTEGER;
+
+-- Step 4: Migrate data - set status_id based on in_progress flag
+-- Set to "In Progress" status if in_progress is true, otherwise "Pending"
+UPDATE task_task
+SET status_id = (
+ CASE
+ WHEN in_progress = true THEN (SELECT id FROM task_taskstatus WHERE name = 'In Progress' LIMIT 1)
+ ELSE (SELECT id FROM task_taskstatus WHERE name = 'Pending' LIMIT 1)
+ END
+);
+
+-- Step 5: Add foreign key constraint
+ALTER TABLE task_task ADD CONSTRAINT fk_task_task_status
+ FOREIGN KEY (status_id) REFERENCES task_taskstatus(id);
+
+-- Step 6: Drop the in_progress column
+ALTER TABLE task_task DROP COLUMN IF EXISTS in_progress;
+
+-- Step 7: Drop the index
+DROP INDEX IF EXISTS idx_task_task_in_progress;
diff --git a/migrations/005_replace_status_with_in_progress.up.sql b/migrations/005_replace_status_with_in_progress.up.sql
new file mode 100644
index 0000000..b16c333
--- /dev/null
+++ b/migrations/005_replace_status_with_in_progress.up.sql
@@ -0,0 +1,44 @@
+-- Migration: Replace status_id foreign key with in_progress boolean
+-- This simplifies the task model since status was only used to determine if a task is "In Progress"
+
+-- Step 1: Add in_progress boolean column with default false
+ALTER TABLE task_task ADD COLUMN IF NOT EXISTS in_progress BOOLEAN NOT NULL DEFAULT false;
+
+-- Step 2: Create index on in_progress for query performance
+CREATE INDEX IF NOT EXISTS idx_task_task_in_progress ON task_task(in_progress);
+
+-- Step 3: Migrate existing data - set in_progress = true for tasks with "In Progress" status
+UPDATE task_task
+SET in_progress = true
+WHERE status_id IN (
+ SELECT id FROM task_taskstatus WHERE LOWER(name) = 'in progress'
+);
+
+-- Step 4: Drop the foreign key constraint on status_id (if it exists)
+-- PostgreSQL syntax - the constraint name might vary
+DO $$
+BEGIN
+ -- Try to drop the constraint if it exists
+ IF EXISTS (
+ SELECT 1 FROM information_schema.table_constraints
+ WHERE constraint_name = 'fk_task_task_status'
+ AND table_name = 'task_task'
+ ) THEN
+ ALTER TABLE task_task DROP CONSTRAINT fk_task_task_status;
+ END IF;
+
+ -- Also try the gorm auto-generated constraint name
+ IF EXISTS (
+ SELECT 1 FROM information_schema.table_constraints
+ WHERE constraint_name = 'task_task_status_id_fkey'
+ AND table_name = 'task_task'
+ ) THEN
+ ALTER TABLE task_task DROP CONSTRAINT task_task_status_id_fkey;
+ END IF;
+END $$;
+
+-- Step 5: Drop the status_id column
+ALTER TABLE task_task DROP COLUMN IF EXISTS status_id;
+
+-- Step 6: Drop the task_taskstatus table
+DROP TABLE IF EXISTS task_taskstatus;