Add performance optimizations and database indexes
Database Indexes (migrations 006-009): - Add case-insensitive indexes for auth lookups (email, username) - Add composite indexes for task kanban queries - Add indexes for notification, document, and completion queries - Add unique index for active share codes - Remove redundant idx_share_code_active and idx_notification_user_sent Repository Optimizations: - Add FindResidenceIDsByUser() lightweight method (IDs only, no preloads) - Optimize GetResidenceUsers() with single UNION query (was 2 queries) - Optimize kanban completion preloads to minimal columns (id, task_id, completed_at) Service Optimizations: - Remove Category/Priority/Frequency preloads from task queries - Remove summary calculations from CRUD responses (client calculates) - Use lightweight FindResidenceIDsByUser() instead of full FindByUser() These changes reduce database load and response times for common operations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,24 @@ func (r *ResidenceRepository) FindByUser(userID uint) ([]models.Residence, error
|
||||
return residences, nil
|
||||
}
|
||||
|
||||
// FindResidenceIDsByUser returns just the IDs of residences a user has access to.
|
||||
// This is a lightweight alternative to FindByUser() when only IDs are needed.
|
||||
// Avoids preloading Owner, Users, PropertyType relations.
|
||||
func (r *ResidenceRepository) FindResidenceIDsByUser(userID uint) ([]uint, error) {
|
||||
var ids []uint
|
||||
err := r.db.Model(&models.Residence{}).
|
||||
Where("is_active = ?", true).
|
||||
Where("owner_id = ? OR id IN (?)",
|
||||
userID,
|
||||
r.db.Table("residence_residence_users").Select("residence_id").Where("user_id = ?", userID),
|
||||
).
|
||||
Pluck("id", &ids).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// FindOwnedByUser finds all residences owned by a user
|
||||
func (r *ResidenceRepository) FindOwnedByUser(userID uint) ([]models.Residence, error) {
|
||||
var residences []models.Residence
|
||||
@@ -118,42 +136,41 @@ func (r *ResidenceRepository) RemoveUser(residenceID, userID uint) error {
|
||||
).Error
|
||||
}
|
||||
|
||||
// GetResidenceUsers returns all users with access to a residence
|
||||
// GetResidenceUsers returns all users with access to a residence (owner + shared users).
|
||||
// Optimized: Uses a single UNION query instead of preloading full residence with relations.
|
||||
func (r *ResidenceRepository) GetResidenceUsers(residenceID uint) ([]models.User, error) {
|
||||
residence, err := r.FindByID(residenceID)
|
||||
var users []models.User
|
||||
// Single query to get both owner and shared users
|
||||
err := r.db.Raw(`
|
||||
SELECT DISTINCT u.* FROM auth_user u
|
||||
WHERE u.id IN (
|
||||
SELECT owner_id FROM residence_residence WHERE id = ? AND is_active = true
|
||||
UNION
|
||||
SELECT user_id FROM residence_residence_users WHERE residence_id = ?
|
||||
)
|
||||
`, residenceID, residenceID).Scan(&users).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users := make([]models.User, 0, len(residence.Users)+1)
|
||||
users = append(users, residence.Owner)
|
||||
users = append(users, residence.Users...)
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// HasAccess checks if a user has access to a residence
|
||||
func (r *ResidenceRepository) HasAccess(residenceID, userID uint) (bool, error) {
|
||||
var count int64
|
||||
|
||||
// Check if user is owner
|
||||
err := r.db.Model(&models.Residence{}).
|
||||
Where("id = ? AND owner_id = ? AND is_active = ?", residenceID, userID, true).
|
||||
Count(&count).Error
|
||||
// Single query using UNION to check owner OR member access
|
||||
err := r.db.Raw(`
|
||||
SELECT COUNT(*) FROM (
|
||||
SELECT 1 FROM residence_residence
|
||||
WHERE id = ? AND owner_id = ? AND is_active = true
|
||||
UNION
|
||||
SELECT 1 FROM residence_residence_users
|
||||
WHERE residence_id = ? AND user_id = ?
|
||||
) access_check
|
||||
`, residenceID, userID, residenceID, userID).Scan(&count).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if count > 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Check if user is in shared users
|
||||
err = r.db.Table("residence_residence_users").
|
||||
Where("residence_id = ? AND user_id = ?", residenceID, userID).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -23,14 +23,12 @@ func NewTaskRepository(db *gorm.DB) *TaskRepository {
|
||||
// === Task CRUD ===
|
||||
|
||||
// FindByID finds a task by ID with preloaded relations
|
||||
// Note: Category, Priority, Frequency are NOT preloaded - client resolves from cache using IDs
|
||||
func (r *TaskRepository) FindByID(id uint) (*models.Task, error) {
|
||||
var task models.Task
|
||||
err := r.db.Preload("Residence").
|
||||
Preload("CreatedBy").
|
||||
Preload("AssignedTo").
|
||||
Preload("Category").
|
||||
Preload("Priority").
|
||||
Preload("Frequency").
|
||||
Preload("Completions").
|
||||
Preload("Completions.Images").
|
||||
Preload("Completions.CompletedBy").
|
||||
@@ -42,13 +40,11 @@ func (r *TaskRepository) FindByID(id uint) (*models.Task, error) {
|
||||
}
|
||||
|
||||
// FindByResidence finds all tasks for a residence
|
||||
// Note: Category, Priority, Frequency are NOT preloaded - client resolves from cache using IDs
|
||||
func (r *TaskRepository) FindByResidence(residenceID uint) ([]models.Task, error) {
|
||||
var tasks []models.Task
|
||||
err := r.db.Preload("CreatedBy").
|
||||
Preload("AssignedTo").
|
||||
Preload("Category").
|
||||
Preload("Priority").
|
||||
Preload("Frequency").
|
||||
Preload("Completions").
|
||||
Preload("Completions.Images").
|
||||
Preload("Completions.CompletedBy").
|
||||
@@ -59,14 +55,12 @@ func (r *TaskRepository) FindByResidence(residenceID uint) ([]models.Task, error
|
||||
}
|
||||
|
||||
// FindByUser finds all tasks accessible to a user (across all their residences)
|
||||
// Note: Category, Priority, Frequency are NOT preloaded - client resolves from cache using IDs
|
||||
func (r *TaskRepository) FindByUser(userID uint, residenceIDs []uint) ([]models.Task, error) {
|
||||
var tasks []models.Task
|
||||
err := r.db.Preload("Residence").
|
||||
Preload("CreatedBy").
|
||||
Preload("AssignedTo").
|
||||
Preload("Category").
|
||||
Preload("Priority").
|
||||
Preload("Frequency").
|
||||
Preload("Completions").
|
||||
Preload("Completions.Images").
|
||||
Preload("Completions.CompletedBy").
|
||||
@@ -134,16 +128,18 @@ func (r *TaskRepository) Unarchive(id uint) error {
|
||||
// GetKanbanData retrieves tasks organized for kanban display.
|
||||
// Uses the task.categorization package as the single source of truth for categorization logic.
|
||||
// The `now` parameter should be the start of day in the user's timezone for accurate overdue detection.
|
||||
//
|
||||
// Optimization: Preloads only minimal completion data (id, task_id, completed_at) for count/detection.
|
||||
// Images and CompletedBy are NOT preloaded - fetch separately when viewing completion details.
|
||||
func (r *TaskRepository) GetKanbanData(residenceID uint, daysThreshold int, now time.Time) (*models.KanbanBoard, error) {
|
||||
var tasks []models.Task
|
||||
// Note: Category, Priority, Frequency are NOT preloaded - client resolves from cache using IDs
|
||||
// Optimization: Preload only minimal Completions data (no Images, no CompletedBy)
|
||||
err := r.db.Preload("CreatedBy").
|
||||
Preload("AssignedTo").
|
||||
Preload("Category").
|
||||
Preload("Priority").
|
||||
Preload("Frequency").
|
||||
Preload("Completions").
|
||||
Preload("Completions.Images").
|
||||
Preload("Completions.CompletedBy").
|
||||
Preload("Completions", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Select("id", "task_id", "completed_at")
|
||||
}).
|
||||
Where("residence_id = ? AND is_archived = ?", residenceID, false).
|
||||
Scopes(task.ScopeKanbanOrder).
|
||||
Find(&tasks).Error
|
||||
@@ -222,17 +218,19 @@ func (r *TaskRepository) GetKanbanData(residenceID uint, daysThreshold int, now
|
||||
// GetKanbanDataForMultipleResidences retrieves tasks from multiple residences organized for kanban display.
|
||||
// Uses the task.categorization package as the single source of truth for categorization logic.
|
||||
// The `now` parameter should be the start of day in the user's timezone for accurate overdue detection.
|
||||
//
|
||||
// Optimization: Preloads only minimal completion data (id, task_id, completed_at) for count/detection.
|
||||
// Images and CompletedBy are NOT preloaded - fetch separately when viewing completion details.
|
||||
func (r *TaskRepository) GetKanbanDataForMultipleResidences(residenceIDs []uint, daysThreshold int, now time.Time) (*models.KanbanBoard, error) {
|
||||
var tasks []models.Task
|
||||
// Note: Category, Priority, Frequency are NOT preloaded - client resolves from cache using IDs
|
||||
// Optimization: Preload only minimal Completions data (no Images, no CompletedBy)
|
||||
err := r.db.Preload("CreatedBy").
|
||||
Preload("AssignedTo").
|
||||
Preload("Category").
|
||||
Preload("Priority").
|
||||
Preload("Frequency").
|
||||
Preload("Completions").
|
||||
Preload("Completions.Images").
|
||||
Preload("Completions.CompletedBy").
|
||||
Preload("Residence").
|
||||
Preload("Completions", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Select("id", "task_id", "completed_at")
|
||||
}).
|
||||
Where("residence_id IN ? AND is_archived = ?", residenceIDs, false).
|
||||
Scopes(task.ScopeKanbanOrder).
|
||||
Find(&tasks).Error
|
||||
@@ -331,6 +329,16 @@ func (r *TaskRepository) GetAllFrequencies() ([]models.TaskFrequency, error) {
|
||||
return frequencies, err
|
||||
}
|
||||
|
||||
// GetFrequencyByID retrieves a single frequency by ID
|
||||
func (r *TaskRepository) GetFrequencyByID(id uint) (*models.TaskFrequency, error) {
|
||||
var frequency models.TaskFrequency
|
||||
err := r.db.First(&frequency, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &frequency, nil
|
||||
}
|
||||
|
||||
// CountByResidence counts tasks in a residence
|
||||
func (r *TaskRepository) CountByResidence(residenceID uint) (int64, error) {
|
||||
var count int64
|
||||
@@ -421,66 +429,70 @@ type TaskStatistics struct {
|
||||
}
|
||||
|
||||
// GetTaskStatistics returns aggregated task statistics for multiple residences.
|
||||
// Uses the task.scopes package for consistent filtering logic.
|
||||
// Uses a single optimized query with CASE statements instead of 5 separate queries.
|
||||
// The `now` parameter should be the start of day in the user's timezone for accurate overdue detection.
|
||||
func (r *TaskRepository) GetTaskStatistics(residenceIDs []uint, now time.Time) (*TaskStatistics, error) {
|
||||
if len(residenceIDs) == 0 {
|
||||
return &TaskStatistics{}, nil
|
||||
}
|
||||
|
||||
var totalTasks, totalOverdue, totalPending, tasksDueNextWeek, tasksDueNextMonth int64
|
||||
nextWeek := now.AddDate(0, 0, 7)
|
||||
nextMonth := now.AddDate(0, 0, 30)
|
||||
|
||||
// Count total active tasks (not cancelled, not archived)
|
||||
// Uses: task.ScopeActive, task.ScopeForResidences
|
||||
// Single query with CASE statements to count all statistics at once
|
||||
// This replaces 5 separate COUNT queries with 1 query
|
||||
type statsResult struct {
|
||||
TotalTasks int64
|
||||
TotalOverdue int64
|
||||
TotalPending int64
|
||||
TasksDueNextWeek int64
|
||||
TasksDueNextMonth int64
|
||||
}
|
||||
|
||||
var result statsResult
|
||||
|
||||
// Build the optimized query
|
||||
// Base conditions: active (not cancelled, not archived), in specified residences
|
||||
// NotCompleted: NOT (next_due_date IS NULL AND has completions)
|
||||
err := r.db.Model(&models.Task{}).
|
||||
Scopes(task.ScopeForResidences(residenceIDs), task.ScopeActive).
|
||||
Count(&totalTasks).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
Select(`
|
||||
COUNT(*) as total_tasks,
|
||||
COUNT(CASE
|
||||
WHEN COALESCE(next_due_date, due_date)::timestamp < ?::timestamp
|
||||
AND NOT (next_due_date IS NULL AND EXISTS (SELECT 1 FROM task_taskcompletion tc WHERE tc.task_id = task_task.id))
|
||||
THEN 1
|
||||
END) as total_overdue,
|
||||
COUNT(CASE
|
||||
WHEN NOT (next_due_date IS NULL AND EXISTS (SELECT 1 FROM task_taskcompletion tc WHERE tc.task_id = task_task.id))
|
||||
THEN 1
|
||||
END) as total_pending,
|
||||
COUNT(CASE
|
||||
WHEN COALESCE(next_due_date, due_date)::timestamp >= ?::timestamp
|
||||
AND COALESCE(next_due_date, due_date)::timestamp < ?::timestamp
|
||||
AND NOT (next_due_date IS NULL AND EXISTS (SELECT 1 FROM task_taskcompletion tc WHERE tc.task_id = task_task.id))
|
||||
THEN 1
|
||||
END) as tasks_due_next_week,
|
||||
COUNT(CASE
|
||||
WHEN COALESCE(next_due_date, due_date)::timestamp >= ?::timestamp
|
||||
AND COALESCE(next_due_date, due_date)::timestamp < ?::timestamp
|
||||
AND NOT (next_due_date IS NULL AND EXISTS (SELECT 1 FROM task_taskcompletion tc WHERE tc.task_id = task_task.id))
|
||||
THEN 1
|
||||
END) as tasks_due_next_month
|
||||
`, now, now, nextWeek, now, nextMonth).
|
||||
Where("residence_id IN ?", residenceIDs).
|
||||
Where("is_cancelled = ? AND is_archived = ?", false, false).
|
||||
Scan(&result).Error
|
||||
|
||||
// Count overdue tasks using consistent scope
|
||||
// Uses: task.ScopeOverdue (which includes ScopeActive and ScopeNotCompleted)
|
||||
err = r.db.Model(&models.Task{}).
|
||||
Scopes(task.ScopeForResidences(residenceIDs), task.ScopeOverdue(now)).
|
||||
Count(&totalOverdue).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Count pending tasks (active, not completed)
|
||||
// Uses: task.ScopeActive, task.ScopeNotCompleted
|
||||
err = r.db.Model(&models.Task{}).
|
||||
Scopes(task.ScopeForResidences(residenceIDs), task.ScopeActive, task.ScopeNotCompleted).
|
||||
Count(&totalPending).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Count tasks due next week using consistent scope
|
||||
// Uses: task.ScopeDueSoon with 7-day threshold
|
||||
err = r.db.Model(&models.Task{}).
|
||||
Scopes(task.ScopeForResidences(residenceIDs), task.ScopeDueSoon(now, 7)).
|
||||
Count(&tasksDueNextWeek).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Count tasks due next month using consistent scope
|
||||
// Uses: task.ScopeDueSoon with 30-day threshold
|
||||
err = r.db.Model(&models.Task{}).
|
||||
Scopes(task.ScopeForResidences(residenceIDs), task.ScopeDueSoon(now, 30)).
|
||||
Count(&tasksDueNextMonth).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TaskStatistics{
|
||||
TotalTasks: int(totalTasks),
|
||||
TotalPending: int(totalPending),
|
||||
TotalOverdue: int(totalOverdue),
|
||||
TasksDueNextWeek: int(tasksDueNextWeek),
|
||||
TasksDueNextMonth: int(tasksDueNextMonth),
|
||||
TotalTasks: int(result.TotalTasks),
|
||||
TotalPending: int(result.TotalPending),
|
||||
TotalOverdue: int(result.TotalOverdue),
|
||||
TasksDueNextWeek: int(result.TasksDueNextWeek),
|
||||
TasksDueNextMonth: int(result.TasksDueNextMonth),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -318,7 +318,7 @@ func TestKanbanBoard_CancelledTasksGoToCancelledColumn(t *testing.T) {
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Cancelled Task")
|
||||
repo.Cancel(task.ID)
|
||||
|
||||
board, err := repo.GetKanbanData(residence.ID, 30)
|
||||
board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Find cancelled column
|
||||
@@ -358,7 +358,7 @@ func TestKanbanBoard_CompletedTasksGoToCompletedColumn(t *testing.T) {
|
||||
err := repo.CreateCompletion(completion)
|
||||
require.NoError(t, err)
|
||||
|
||||
board, err := repo.GetKanbanData(residence.ID, 30)
|
||||
board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Find completed column
|
||||
@@ -397,7 +397,7 @@ func TestKanbanBoard_InProgressTasksGoToInProgressColumn(t *testing.T) {
|
||||
err := db.Create(task).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
board, err := repo.GetKanbanData(residence.ID, 30)
|
||||
board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Find in_progress column
|
||||
@@ -437,7 +437,7 @@ func TestKanbanBoard_OverdueTasksGoToOverdueColumn(t *testing.T) {
|
||||
err := db.Create(task).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
board, err := repo.GetKanbanData(residence.ID, 30)
|
||||
board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Find overdue column
|
||||
@@ -477,7 +477,7 @@ func TestKanbanBoard_DueSoonTasksGoToDueSoonColumn(t *testing.T) {
|
||||
err := db.Create(task).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
board, err := repo.GetKanbanData(residence.ID, 30)
|
||||
board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Find due_soon column
|
||||
@@ -517,7 +517,7 @@ func TestKanbanBoard_UpcomingTasksGoToUpcomingColumn(t *testing.T) {
|
||||
err := db.Create(task).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
board, err := repo.GetKanbanData(residence.ID, 30)
|
||||
board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Find upcoming column
|
||||
@@ -549,7 +549,7 @@ func TestKanbanBoard_TasksWithNoDueDateGoToUpcomingColumn(t *testing.T) {
|
||||
// Create a task with no due date
|
||||
task := testutil.CreateTestTask(t, db, residence.ID, user.ID, "No Due Date Task")
|
||||
|
||||
board, err := repo.GetKanbanData(residence.ID, 30)
|
||||
board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Find upcoming column
|
||||
@@ -579,7 +579,7 @@ func TestKanbanBoard_ArchivedTasksAreExcluded(t *testing.T) {
|
||||
archivedTask := testutil.CreateTestTask(t, db, residence.ID, user.ID, "Archived Task")
|
||||
repo.Archive(archivedTask.ID)
|
||||
|
||||
board, err := repo.GetKanbanData(residence.ID, 30)
|
||||
board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Count total tasks across all columns
|
||||
@@ -613,7 +613,7 @@ func TestKanbanBoard_CategoryPriority_CancelledTakesPrecedence(t *testing.T) {
|
||||
err := db.Create(task).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
board, err := repo.GetKanbanData(residence.ID, 30)
|
||||
board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Find cancelled column
|
||||
@@ -661,7 +661,7 @@ func TestKanbanBoard_CategoryPriority_CompletedTakesPrecedenceOverInProgress(t *
|
||||
err = repo.CreateCompletion(completion)
|
||||
require.NoError(t, err)
|
||||
|
||||
board, err := repo.GetKanbanData(residence.ID, 30)
|
||||
board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Find columns
|
||||
@@ -700,7 +700,7 @@ func TestKanbanBoard_DaysThresholdAffectsCategorization(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// With 30-day threshold, should be in "due_soon"
|
||||
board30, err := repo.GetKanbanData(residence.ID, 30)
|
||||
board30, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
|
||||
require.NoError(t, err)
|
||||
|
||||
var dueSoon30, upcoming30 *models.KanbanColumn
|
||||
@@ -716,7 +716,7 @@ func TestKanbanBoard_DaysThresholdAffectsCategorization(t *testing.T) {
|
||||
assert.Equal(t, 0, upcoming30.Count, "With 30-day threshold, task should NOT be in upcoming")
|
||||
|
||||
// With 5-day threshold, should be in "upcoming"
|
||||
board5, err := repo.GetKanbanData(residence.ID, 5)
|
||||
board5, err := repo.GetKanbanData(residence.ID, 5, time.Now().UTC())
|
||||
require.NoError(t, err)
|
||||
|
||||
var dueSoon5, upcoming5 *models.KanbanColumn
|
||||
@@ -740,7 +740,7 @@ func TestKanbanBoard_ColumnMetadata(t *testing.T) {
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
|
||||
board, err := repo.GetKanbanData(residence.ID, 30)
|
||||
board, err := repo.GetKanbanData(residence.ID, 30, time.Now().UTC())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify all 6 columns exist with correct metadata
|
||||
@@ -793,7 +793,7 @@ func TestKanbanBoard_MultipleResidences(t *testing.T) {
|
||||
cancelledTask := testutil.CreateTestTask(t, db, residence1.ID, user.ID, "Cancelled in House 1")
|
||||
repo.Cancel(cancelledTask.ID)
|
||||
|
||||
board, err := repo.GetKanbanDataForMultipleResidences([]uint{residence1.ID, residence2.ID}, 30)
|
||||
board, err := repo.GetKanbanDataForMultipleResidences([]uint{residence1.ID, residence2.ID}, 30, time.Now().UTC())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Count total tasks
|
||||
|
||||
Reference in New Issue
Block a user