Add per-residence overdue_count to API response

Adds overdueCount field to each residence in my-residences endpoint,
enabling the mobile app to show pulsing icons on individual residence
cards that have overdue tasks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-07 12:19:11 -06:00
parent cfb8a28870
commit a348f31a9e
3 changed files with 69 additions and 24 deletions

View File

@@ -25,31 +25,32 @@ type ResidenceUserResponse struct {
// ResidenceResponse represents a residence in the API response // ResidenceResponse represents a residence in the API response
type ResidenceResponse struct { type ResidenceResponse struct {
ID uint `json:"id"` ID uint `json:"id"`
OwnerID uint `json:"owner_id"` OwnerID uint `json:"owner_id"`
Owner *ResidenceUserResponse `json:"owner,omitempty"` Owner *ResidenceUserResponse `json:"owner,omitempty"`
Users []ResidenceUserResponse `json:"users,omitempty"` Users []ResidenceUserResponse `json:"users,omitempty"`
Name string `json:"name"` Name string `json:"name"`
PropertyTypeID *uint `json:"property_type_id"` PropertyTypeID *uint `json:"property_type_id"`
PropertyType *ResidenceTypeResponse `json:"property_type,omitempty"` PropertyType *ResidenceTypeResponse `json:"property_type,omitempty"`
StreetAddress string `json:"street_address"` StreetAddress string `json:"street_address"`
ApartmentUnit string `json:"apartment_unit"` ApartmentUnit string `json:"apartment_unit"`
City string `json:"city"` City string `json:"city"`
StateProvince string `json:"state_province"` StateProvince string `json:"state_province"`
PostalCode string `json:"postal_code"` PostalCode string `json:"postal_code"`
Country string `json:"country"` Country string `json:"country"`
Bedrooms *int `json:"bedrooms"` Bedrooms *int `json:"bedrooms"`
Bathrooms *decimal.Decimal `json:"bathrooms"` Bathrooms *decimal.Decimal `json:"bathrooms"`
SquareFootage *int `json:"square_footage"` SquareFootage *int `json:"square_footage"`
LotSize *decimal.Decimal `json:"lot_size"` LotSize *decimal.Decimal `json:"lot_size"`
YearBuilt *int `json:"year_built"` YearBuilt *int `json:"year_built"`
Description string `json:"description"` Description string `json:"description"`
PurchaseDate *time.Time `json:"purchase_date"` PurchaseDate *time.Time `json:"purchase_date"`
PurchasePrice *decimal.Decimal `json:"purchase_price"` PurchasePrice *decimal.Decimal `json:"purchase_price"`
IsPrimary bool `json:"is_primary"` IsPrimary bool `json:"is_primary"`
IsActive bool `json:"is_active"` IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"` OverdueCount int `json:"overdue_count"`
UpdatedAt time.Time `json:"updated_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
} }
// TotalSummary represents summary statistics for all residences // TotalSummary represents summary statistics for all residences

View File

@@ -502,3 +502,37 @@ func (r *TaskRepository) GetTaskStatistics(residenceIDs []uint) (*TaskStatistics
TasksDueNextMonth: int(tasksDueNextMonth), TasksDueNextMonth: int(tasksDueNextMonth),
}, nil }, nil
} }
// GetOverdueCountByResidence returns a map of residence ID to overdue task count.
// Uses the task.scopes package for consistent filtering logic.
func (r *TaskRepository) GetOverdueCountByResidence(residenceIDs []uint) (map[uint]int, error) {
if len(residenceIDs) == 0 {
return map[uint]int{}, nil
}
now := time.Now().UTC()
// 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
}

View File

@@ -113,6 +113,16 @@ func (s *ResidenceService) GetMyResidences(userID uint) (*responses.MyResidences
summary.TasksDueNextWeek = stats.TasksDueNextWeek summary.TasksDueNextWeek = stats.TasksDueNextWeek
summary.TasksDueNextMonth = stats.TasksDueNextMonth summary.TasksDueNextMonth = stats.TasksDueNextMonth
} }
// Get per-residence overdue counts
overdueCounts, err := s.taskRepo.GetOverdueCountByResidence(residenceIDs)
if err == nil && overdueCounts != nil {
for i := range residenceResponses {
if count, ok := overdueCounts[residenceResponses[i].ID]; ok {
residenceResponses[i].OverdueCount = count
}
}
}
} }
return &responses.MyResidencesResponse{ return &responses.MyResidencesResponse{