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>
531 lines
18 KiB
Go
531 lines
18 KiB
Go
package repositories
|
|
|
|
import (
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/treytartt/casera-api/internal/models"
|
|
"github.com/treytartt/casera-api/internal/task"
|
|
"github.com/treytartt/casera-api/internal/task/categorization"
|
|
)
|
|
|
|
// TaskRepository handles database operations for tasks
|
|
type TaskRepository struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
// NewTaskRepository creates a new task repository
|
|
func NewTaskRepository(db *gorm.DB) *TaskRepository {
|
|
return &TaskRepository{db: db}
|
|
}
|
|
|
|
// === 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("Completions").
|
|
Preload("Completions.Images").
|
|
Preload("Completions.CompletedBy").
|
|
First(&task, id).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &task, nil
|
|
}
|
|
|
|
// 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("Completions").
|
|
Preload("Completions.Images").
|
|
Preload("Completions.CompletedBy").
|
|
Where("residence_id = ?", residenceID).
|
|
Order("due_date ASC NULLS LAST, created_at DESC").
|
|
Find(&tasks).Error
|
|
return tasks, err
|
|
}
|
|
|
|
// 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("Completions").
|
|
Preload("Completions.Images").
|
|
Preload("Completions.CompletedBy").
|
|
Where("residence_id IN ?", residenceIDs).
|
|
Order("due_date ASC NULLS LAST, created_at DESC").
|
|
Find(&tasks).Error
|
|
return tasks, err
|
|
}
|
|
|
|
// Create creates a new task
|
|
func (r *TaskRepository) Create(task *models.Task) error {
|
|
return r.db.Create(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", "Frequency", "ParentTask", "Completions").Save(task).Error
|
|
}
|
|
|
|
// Delete hard-deletes a task
|
|
func (r *TaskRepository) Delete(id uint) error {
|
|
return r.db.Delete(&models.Task{}, id).Error
|
|
}
|
|
|
|
// === Task State Operations ===
|
|
|
|
// MarkInProgress marks a task as in progress
|
|
func (r *TaskRepository) MarkInProgress(id uint) error {
|
|
return r.db.Model(&models.Task{}).
|
|
Where("id = ?", id).
|
|
Update("in_progress", true).Error
|
|
}
|
|
|
|
// Cancel cancels a task
|
|
func (r *TaskRepository) Cancel(id uint) error {
|
|
return r.db.Model(&models.Task{}).
|
|
Where("id = ?", id).
|
|
Update("is_cancelled", true).Error
|
|
}
|
|
|
|
// Uncancel uncancels a task
|
|
func (r *TaskRepository) Uncancel(id uint) error {
|
|
return r.db.Model(&models.Task{}).
|
|
Where("id = ?", id).
|
|
Update("is_cancelled", false).Error
|
|
}
|
|
|
|
// Archive archives a task
|
|
func (r *TaskRepository) Archive(id uint) error {
|
|
return r.db.Model(&models.Task{}).
|
|
Where("id = ?", id).
|
|
Update("is_archived", true).Error
|
|
}
|
|
|
|
// Unarchive unarchives a task
|
|
func (r *TaskRepository) Unarchive(id uint) error {
|
|
return r.db.Model(&models.Task{}).
|
|
Where("id = ?", id).
|
|
Update("is_archived", false).Error
|
|
}
|
|
|
|
// === Kanban Board ===
|
|
|
|
// 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("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
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Use the categorization package as the single source of truth
|
|
// Pass the user's timezone-aware time for accurate overdue detection
|
|
categorized := categorization.CategorizeTasksIntoColumnsWithTime(tasks, daysThreshold, now)
|
|
|
|
columns := []models.KanbanColumn{
|
|
{
|
|
Name: string(categorization.ColumnOverdue),
|
|
DisplayName: "Overdue",
|
|
ButtonTypes: []string{"edit", "complete", "cancel", "mark_in_progress"},
|
|
Icons: map[string]string{"ios": "exclamationmark.triangle", "android": "Warning"},
|
|
Color: "#FF3B30",
|
|
Tasks: categorized[categorization.ColumnOverdue],
|
|
Count: len(categorized[categorization.ColumnOverdue]),
|
|
},
|
|
{
|
|
Name: string(categorization.ColumnInProgress),
|
|
DisplayName: "In Progress",
|
|
ButtonTypes: []string{"edit", "complete", "cancel"},
|
|
Icons: map[string]string{"ios": "hammer", "android": "Build"},
|
|
Color: "#5856D6",
|
|
Tasks: categorized[categorization.ColumnInProgress],
|
|
Count: len(categorized[categorization.ColumnInProgress]),
|
|
},
|
|
{
|
|
Name: string(categorization.ColumnDueSoon),
|
|
DisplayName: "Due Soon",
|
|
ButtonTypes: []string{"edit", "complete", "cancel", "mark_in_progress"},
|
|
Icons: map[string]string{"ios": "clock", "android": "Schedule"},
|
|
Color: "#FF9500",
|
|
Tasks: categorized[categorization.ColumnDueSoon],
|
|
Count: len(categorized[categorization.ColumnDueSoon]),
|
|
},
|
|
{
|
|
Name: string(categorization.ColumnUpcoming),
|
|
DisplayName: "Upcoming",
|
|
ButtonTypes: []string{"edit", "complete", "cancel", "mark_in_progress"},
|
|
Icons: map[string]string{"ios": "calendar", "android": "Event"},
|
|
Color: "#007AFF",
|
|
Tasks: categorized[categorization.ColumnUpcoming],
|
|
Count: len(categorized[categorization.ColumnUpcoming]),
|
|
},
|
|
{
|
|
Name: string(categorization.ColumnCompleted),
|
|
DisplayName: "Completed",
|
|
ButtonTypes: []string{},
|
|
Icons: map[string]string{"ios": "checkmark.circle", "android": "CheckCircle"},
|
|
Color: "#34C759",
|
|
Tasks: categorized[categorization.ColumnCompleted],
|
|
Count: len(categorized[categorization.ColumnCompleted]),
|
|
},
|
|
{
|
|
Name: string(categorization.ColumnCancelled),
|
|
DisplayName: "Cancelled",
|
|
ButtonTypes: []string{"uncancel", "delete"},
|
|
Icons: map[string]string{"ios": "xmark.circle", "android": "Cancel"},
|
|
Color: "#8E8E93",
|
|
Tasks: categorized[categorization.ColumnCancelled],
|
|
Count: len(categorized[categorization.ColumnCancelled]),
|
|
},
|
|
}
|
|
|
|
return &models.KanbanBoard{
|
|
Columns: columns,
|
|
DaysThreshold: daysThreshold,
|
|
ResidenceID: string(rune(residenceID)),
|
|
}, nil
|
|
}
|
|
|
|
// 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("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
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Use the categorization package as the single source of truth
|
|
// Pass the user's timezone-aware time for accurate overdue detection
|
|
categorized := categorization.CategorizeTasksIntoColumnsWithTime(tasks, daysThreshold, now)
|
|
|
|
columns := []models.KanbanColumn{
|
|
{
|
|
Name: string(categorization.ColumnOverdue),
|
|
DisplayName: "Overdue",
|
|
ButtonTypes: []string{"edit", "complete", "cancel", "mark_in_progress"},
|
|
Icons: map[string]string{"ios": "exclamationmark.triangle", "android": "Warning"},
|
|
Color: "#FF3B30",
|
|
Tasks: categorized[categorization.ColumnOverdue],
|
|
Count: len(categorized[categorization.ColumnOverdue]),
|
|
},
|
|
{
|
|
Name: string(categorization.ColumnInProgress),
|
|
DisplayName: "In Progress",
|
|
ButtonTypes: []string{"edit", "complete", "cancel"},
|
|
Icons: map[string]string{"ios": "hammer", "android": "Build"},
|
|
Color: "#5856D6",
|
|
Tasks: categorized[categorization.ColumnInProgress],
|
|
Count: len(categorized[categorization.ColumnInProgress]),
|
|
},
|
|
{
|
|
Name: string(categorization.ColumnDueSoon),
|
|
DisplayName: "Due Soon",
|
|
ButtonTypes: []string{"edit", "complete", "cancel", "mark_in_progress"},
|
|
Icons: map[string]string{"ios": "clock", "android": "Schedule"},
|
|
Color: "#FF9500",
|
|
Tasks: categorized[categorization.ColumnDueSoon],
|
|
Count: len(categorized[categorization.ColumnDueSoon]),
|
|
},
|
|
{
|
|
Name: string(categorization.ColumnUpcoming),
|
|
DisplayName: "Upcoming",
|
|
ButtonTypes: []string{"edit", "complete", "cancel", "mark_in_progress"},
|
|
Icons: map[string]string{"ios": "calendar", "android": "Event"},
|
|
Color: "#007AFF",
|
|
Tasks: categorized[categorization.ColumnUpcoming],
|
|
Count: len(categorized[categorization.ColumnUpcoming]),
|
|
},
|
|
{
|
|
Name: string(categorization.ColumnCompleted),
|
|
DisplayName: "Completed",
|
|
ButtonTypes: []string{},
|
|
Icons: map[string]string{"ios": "checkmark.circle", "android": "CheckCircle"},
|
|
Color: "#34C759",
|
|
Tasks: categorized[categorization.ColumnCompleted],
|
|
Count: len(categorized[categorization.ColumnCompleted]),
|
|
},
|
|
{
|
|
Name: string(categorization.ColumnCancelled),
|
|
DisplayName: "Cancelled",
|
|
ButtonTypes: []string{"uncancel", "delete"},
|
|
Icons: map[string]string{"ios": "xmark.circle", "android": "Cancel"},
|
|
Color: "#8E8E93",
|
|
Tasks: categorized[categorization.ColumnCancelled],
|
|
Count: len(categorized[categorization.ColumnCancelled]),
|
|
},
|
|
}
|
|
|
|
return &models.KanbanBoard{
|
|
Columns: columns,
|
|
DaysThreshold: daysThreshold,
|
|
ResidenceID: "all",
|
|
}, nil
|
|
}
|
|
|
|
// === Lookup Operations ===
|
|
|
|
// GetAllCategories returns all task categories
|
|
func (r *TaskRepository) GetAllCategories() ([]models.TaskCategory, error) {
|
|
var categories []models.TaskCategory
|
|
err := r.db.Order("display_order, name").Find(&categories).Error
|
|
return categories, err
|
|
}
|
|
|
|
// GetAllPriorities returns all task priorities
|
|
func (r *TaskRepository) GetAllPriorities() ([]models.TaskPriority, error) {
|
|
var priorities []models.TaskPriority
|
|
err := r.db.Order("level").Find(&priorities).Error
|
|
return priorities, err
|
|
}
|
|
|
|
// GetAllFrequencies returns all task frequencies
|
|
func (r *TaskRepository) GetAllFrequencies() ([]models.TaskFrequency, error) {
|
|
var frequencies []models.TaskFrequency
|
|
err := r.db.Order("display_order").Find(&frequencies).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
|
|
err := r.db.Model(&models.Task{}).
|
|
Where("residence_id = ? AND is_cancelled = ? AND is_archived = ?", residenceID, false, false).
|
|
Count(&count).Error
|
|
return count, err
|
|
}
|
|
|
|
// === Task Completion Operations ===
|
|
|
|
// CreateCompletion creates a new task completion
|
|
func (r *TaskRepository) CreateCompletion(completion *models.TaskCompletion) error {
|
|
return r.db.Create(completion).Error
|
|
}
|
|
|
|
// FindCompletionByID finds a completion by ID
|
|
func (r *TaskRepository) FindCompletionByID(id uint) (*models.TaskCompletion, error) {
|
|
var completion models.TaskCompletion
|
|
err := r.db.Preload("Task").
|
|
Preload("CompletedBy").
|
|
Preload("Images").
|
|
First(&completion, id).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &completion, nil
|
|
}
|
|
|
|
// FindCompletionsByTask finds all completions for a task
|
|
func (r *TaskRepository) FindCompletionsByTask(taskID uint) ([]models.TaskCompletion, error) {
|
|
var completions []models.TaskCompletion
|
|
err := r.db.Preload("CompletedBy").
|
|
Preload("Images").
|
|
Where("task_id = ?", taskID).
|
|
Order("completed_at DESC").
|
|
Find(&completions).Error
|
|
return completions, err
|
|
}
|
|
|
|
// FindCompletionsByUser finds all completions by a user
|
|
func (r *TaskRepository) FindCompletionsByUser(userID uint, residenceIDs []uint) ([]models.TaskCompletion, error) {
|
|
var completions []models.TaskCompletion
|
|
err := r.db.Preload("Task").
|
|
Preload("CompletedBy").
|
|
Preload("Images").
|
|
Joins("JOIN task_task ON task_task.id = task_taskcompletion.task_id").
|
|
Where("task_task.residence_id IN ?", residenceIDs).
|
|
Order("completed_at DESC").
|
|
Find(&completions).Error
|
|
return completions, err
|
|
}
|
|
|
|
// DeleteCompletion deletes a task completion
|
|
func (r *TaskRepository) DeleteCompletion(id uint) error {
|
|
// Delete images first
|
|
r.db.Where("completion_id = ?", id).Delete(&models.TaskCompletionImage{})
|
|
return r.db.Delete(&models.TaskCompletion{}, id).Error
|
|
}
|
|
|
|
// CreateCompletionImage creates a new completion image
|
|
func (r *TaskRepository) CreateCompletionImage(image *models.TaskCompletionImage) error {
|
|
return r.db.Create(image).Error
|
|
}
|
|
|
|
// DeleteCompletionImage deletes a completion image
|
|
func (r *TaskRepository) DeleteCompletionImage(id uint) error {
|
|
return r.db.Delete(&models.TaskCompletionImage{}, id).Error
|
|
}
|
|
|
|
// FindCompletionImageByID finds a completion image by ID
|
|
func (r *TaskRepository) FindCompletionImageByID(id uint) (*models.TaskCompletionImage, error) {
|
|
var image models.TaskCompletionImage
|
|
err := r.db.First(&image, id).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &image, nil
|
|
}
|
|
|
|
// TaskStatistics represents aggregated task statistics
|
|
type TaskStatistics struct {
|
|
TotalTasks int
|
|
TotalPending int
|
|
TotalOverdue int
|
|
TasksDueNextWeek int
|
|
TasksDueNextMonth int
|
|
}
|
|
|
|
// GetTaskStatistics returns aggregated task statistics for multiple residences.
|
|
// 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
|
|
}
|
|
|
|
nextWeek := now.AddDate(0, 0, 7)
|
|
nextMonth := now.AddDate(0, 0, 30)
|
|
|
|
// 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{}).
|
|
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
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &TaskStatistics{
|
|
TotalTasks: int(result.TotalTasks),
|
|
TotalPending: int(result.TotalPending),
|
|
TotalOverdue: int(result.TotalOverdue),
|
|
TasksDueNextWeek: int(result.TasksDueNextWeek),
|
|
TasksDueNextMonth: int(result.TasksDueNextMonth),
|
|
}, nil
|
|
}
|
|
|
|
// GetOverdueCountByResidence returns a map of residence ID to overdue task count.
|
|
// Uses the task.scopes package for consistent filtering logic.
|
|
// The `now` parameter should be the start of day in the user's timezone for accurate overdue detection.
|
|
func (r *TaskRepository) GetOverdueCountByResidence(residenceIDs []uint, now time.Time) (map[uint]int, error) {
|
|
if len(residenceIDs) == 0 {
|
|
return map[uint]int{}, nil
|
|
}
|
|
|
|
// Query to get overdue count grouped by residence
|
|
type result struct {
|
|
ResidenceID uint
|
|
Count int64
|
|
}
|
|
var results []result
|
|
|
|
err := r.db.Model(&models.Task{}).
|
|
Select("residence_id, COUNT(*) as count").
|
|
Scopes(task.ScopeForResidences(residenceIDs), task.ScopeOverdue(now)).
|
|
Group("residence_id").
|
|
Scan(&results).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert to map
|
|
countMap := make(map[uint]int)
|
|
for _, r := range results {
|
|
countMap[r.ResidenceID] = int(r.Count)
|
|
}
|
|
|
|
return countMap, nil
|
|
}
|