14 new optional residence fields (heating, cooling, water heater, roof, pool, sprinkler, septic, fireplace, garage, basement, attic, exterior, flooring, landscaping) with JSONB conditions on templates. Suggestion engine scores templates against home profile: string match +0.25, bool +0.3, property type +0.15, universal base 0.3. Graceful degradation from minimal to full profile info. GET /api/tasks/suggestions/?residence_id=X returns ranked templates. 54 template conditions across 44 templates in seed data. 8 suggestion service tests.
268 lines
9.6 KiB
Go
268 lines
9.6 KiB
Go
package responses
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/shopspring/decimal"
|
|
|
|
"github.com/treytartt/honeydue-api/internal/models"
|
|
)
|
|
|
|
// ResidenceTypeResponse represents a residence type in the API response
|
|
type ResidenceTypeResponse struct {
|
|
ID uint `json:"id"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// ResidenceUserResponse represents a user with access to a residence
|
|
type ResidenceUserResponse struct {
|
|
ID uint `json:"id"`
|
|
Username string `json:"username"`
|
|
Email string `json:"email"`
|
|
FirstName string `json:"first_name"`
|
|
LastName string `json:"last_name"`
|
|
}
|
|
|
|
// ResidenceResponse represents a residence in the API response
|
|
type ResidenceResponse struct {
|
|
ID uint `json:"id"`
|
|
OwnerID uint `json:"owner_id"`
|
|
Owner *ResidenceUserResponse `json:"owner,omitempty"`
|
|
Users []ResidenceUserResponse `json:"users,omitempty"`
|
|
Name string `json:"name"`
|
|
PropertyTypeID *uint `json:"property_type_id"`
|
|
PropertyType *ResidenceTypeResponse `json:"property_type,omitempty"`
|
|
StreetAddress string `json:"street_address"`
|
|
ApartmentUnit string `json:"apartment_unit"`
|
|
City string `json:"city"`
|
|
StateProvince string `json:"state_province"`
|
|
PostalCode string `json:"postal_code"`
|
|
Country string `json:"country"`
|
|
Bedrooms *int `json:"bedrooms"`
|
|
Bathrooms *decimal.Decimal `json:"bathrooms"`
|
|
SquareFootage *int `json:"square_footage"`
|
|
LotSize *decimal.Decimal `json:"lot_size"`
|
|
YearBuilt *int `json:"year_built"`
|
|
Description string `json:"description"`
|
|
PurchaseDate *time.Time `json:"purchase_date"`
|
|
PurchasePrice *decimal.Decimal `json:"purchase_price"`
|
|
// Home Profile
|
|
HeatingType *string `json:"heating_type"`
|
|
CoolingType *string `json:"cooling_type"`
|
|
WaterHeaterType *string `json:"water_heater_type"`
|
|
RoofType *string `json:"roof_type"`
|
|
HasPool bool `json:"has_pool"`
|
|
HasSprinklerSystem bool `json:"has_sprinkler_system"`
|
|
HasSeptic bool `json:"has_septic"`
|
|
HasFireplace bool `json:"has_fireplace"`
|
|
HasGarage bool `json:"has_garage"`
|
|
HasBasement bool `json:"has_basement"`
|
|
HasAttic bool `json:"has_attic"`
|
|
ExteriorType *string `json:"exterior_type"`
|
|
FlooringPrimary *string `json:"flooring_primary"`
|
|
LandscapingType *string `json:"landscaping_type"`
|
|
|
|
IsPrimary bool `json:"is_primary"`
|
|
IsActive bool `json:"is_active"`
|
|
OverdueCount int `json:"overdue_count"`
|
|
CompletionSummary *CompletionSummary `json:"completion_summary,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// TotalSummary represents summary statistics for all residences
|
|
type TotalSummary struct {
|
|
TotalResidences int `json:"total_residences"`
|
|
TotalTasks int `json:"total_tasks"`
|
|
TotalPending int `json:"total_pending"`
|
|
TotalOverdue int `json:"total_overdue"`
|
|
TasksDueNextWeek int `json:"tasks_due_next_week"`
|
|
TasksDueNextMonth int `json:"tasks_due_next_month"`
|
|
}
|
|
|
|
// MyResidencesResponse represents the response for my-residences endpoint
|
|
// NOTE: Summary statistics are calculated client-side from kanban data
|
|
type MyResidencesResponse struct {
|
|
Residences []ResidenceResponse `json:"residences"`
|
|
}
|
|
|
|
// ResidenceWithSummaryResponse wraps ResidenceResponse with TotalSummary for CRUD operations
|
|
type ResidenceWithSummaryResponse struct {
|
|
Data ResidenceResponse `json:"data"`
|
|
Summary TotalSummary `json:"summary"`
|
|
}
|
|
|
|
// ResidenceDeleteWithSummaryResponse for delete operations
|
|
type ResidenceDeleteWithSummaryResponse struct {
|
|
Data string `json:"data"`
|
|
Summary TotalSummary `json:"summary"`
|
|
}
|
|
|
|
// ShareCodeResponse represents a share code in the API response
|
|
type ShareCodeResponse struct {
|
|
ID uint `json:"id"`
|
|
Code string `json:"code"`
|
|
ResidenceID uint `json:"residence_id"`
|
|
CreatedByID uint `json:"created_by_id"`
|
|
IsActive bool `json:"is_active"`
|
|
ExpiresAt *time.Time `json:"expires_at"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// JoinResidenceResponse represents the response after joining a residence
|
|
type JoinResidenceResponse struct {
|
|
Message string `json:"message"`
|
|
Residence ResidenceResponse `json:"residence"`
|
|
Summary TotalSummary `json:"summary"`
|
|
}
|
|
|
|
// GenerateShareCodeResponse represents the response after generating a share code
|
|
type GenerateShareCodeResponse struct {
|
|
Message string `json:"message"`
|
|
ShareCode ShareCodeResponse `json:"share_code"`
|
|
}
|
|
|
|
// SharePackageResponse represents the response for generating a share package
|
|
// This contains the share code plus metadata for the .honeydue file
|
|
type SharePackageResponse struct {
|
|
ShareCode string `json:"share_code"`
|
|
ResidenceName string `json:"residence_name"`
|
|
SharedBy string `json:"shared_by"`
|
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
|
}
|
|
|
|
// ColumnCompletionCount represents completions from a specific kanban column
|
|
type ColumnCompletionCount struct {
|
|
Column string `json:"column"`
|
|
Color string `json:"color"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
// MonthlyCompletionSummary represents completions for a single month
|
|
type MonthlyCompletionSummary struct {
|
|
Month string `json:"month"` // "2025-04" format
|
|
Completions []ColumnCompletionCount `json:"completions"`
|
|
Total int `json:"total"`
|
|
Overflow int `json:"overflow"` // completions beyond the display cap
|
|
}
|
|
|
|
// CompletionSummary represents task completion data for the honeycomb grid
|
|
type CompletionSummary struct {
|
|
TotalAllTime int `json:"total_all_time"`
|
|
TotalLast12Months int `json:"total_last_12_months"`
|
|
Months []MonthlyCompletionSummary `json:"months"`
|
|
}
|
|
|
|
// === Factory Functions ===
|
|
|
|
// NewResidenceUserResponse creates a ResidenceUserResponse from a User model
|
|
func NewResidenceUserResponse(user *models.User) *ResidenceUserResponse {
|
|
if user == nil {
|
|
return nil
|
|
}
|
|
return &ResidenceUserResponse{
|
|
ID: user.ID,
|
|
Username: user.Username,
|
|
Email: user.Email,
|
|
FirstName: user.FirstName,
|
|
LastName: user.LastName,
|
|
}
|
|
}
|
|
|
|
// NewResidenceTypeResponse creates a ResidenceTypeResponse from a ResidenceType model
|
|
func NewResidenceTypeResponse(rt *models.ResidenceType) *ResidenceTypeResponse {
|
|
if rt == nil {
|
|
return nil
|
|
}
|
|
return &ResidenceTypeResponse{
|
|
ID: rt.ID,
|
|
Name: rt.Name,
|
|
}
|
|
}
|
|
|
|
// NewResidenceResponse creates a ResidenceResponse from a Residence model
|
|
func NewResidenceResponse(residence *models.Residence) ResidenceResponse {
|
|
resp := ResidenceResponse{
|
|
ID: residence.ID,
|
|
OwnerID: residence.OwnerID,
|
|
Name: residence.Name,
|
|
PropertyTypeID: residence.PropertyTypeID,
|
|
StreetAddress: residence.StreetAddress,
|
|
ApartmentUnit: residence.ApartmentUnit,
|
|
City: residence.City,
|
|
StateProvince: residence.StateProvince,
|
|
PostalCode: residence.PostalCode,
|
|
Country: residence.Country,
|
|
Bedrooms: residence.Bedrooms,
|
|
Bathrooms: residence.Bathrooms,
|
|
SquareFootage: residence.SquareFootage,
|
|
LotSize: residence.LotSize,
|
|
YearBuilt: residence.YearBuilt,
|
|
Description: residence.Description,
|
|
PurchaseDate: residence.PurchaseDate,
|
|
PurchasePrice: residence.PurchasePrice,
|
|
HeatingType: residence.HeatingType,
|
|
CoolingType: residence.CoolingType,
|
|
WaterHeaterType: residence.WaterHeaterType,
|
|
RoofType: residence.RoofType,
|
|
HasPool: residence.HasPool,
|
|
HasSprinklerSystem: residence.HasSprinklerSystem,
|
|
HasSeptic: residence.HasSeptic,
|
|
HasFireplace: residence.HasFireplace,
|
|
HasGarage: residence.HasGarage,
|
|
HasBasement: residence.HasBasement,
|
|
HasAttic: residence.HasAttic,
|
|
ExteriorType: residence.ExteriorType,
|
|
FlooringPrimary: residence.FlooringPrimary,
|
|
LandscapingType: residence.LandscapingType,
|
|
IsPrimary: residence.IsPrimary,
|
|
IsActive: residence.IsActive,
|
|
CreatedAt: residence.CreatedAt,
|
|
UpdatedAt: residence.UpdatedAt,
|
|
}
|
|
|
|
// Include owner if loaded
|
|
if residence.Owner.ID != 0 {
|
|
resp.Owner = NewResidenceUserResponse(&residence.Owner)
|
|
}
|
|
|
|
// Include property type if loaded
|
|
if residence.PropertyType != nil {
|
|
resp.PropertyType = NewResidenceTypeResponse(residence.PropertyType)
|
|
}
|
|
|
|
// Include shared users if loaded
|
|
if len(residence.Users) > 0 {
|
|
resp.Users = make([]ResidenceUserResponse, len(residence.Users))
|
|
for i, user := range residence.Users {
|
|
resp.Users[i] = *NewResidenceUserResponse(&user)
|
|
}
|
|
} else {
|
|
resp.Users = []ResidenceUserResponse{}
|
|
}
|
|
|
|
return resp
|
|
}
|
|
|
|
// NewResidenceListResponse creates a list of residence responses
|
|
func NewResidenceListResponse(residences []models.Residence) []ResidenceResponse {
|
|
results := make([]ResidenceResponse, len(residences))
|
|
for i, r := range residences {
|
|
results[i] = NewResidenceResponse(&r)
|
|
}
|
|
return results
|
|
}
|
|
|
|
// NewShareCodeResponse creates a ShareCodeResponse from a ResidenceShareCode model
|
|
func NewShareCodeResponse(sc *models.ResidenceShareCode) ShareCodeResponse {
|
|
return ShareCodeResponse{
|
|
ID: sc.ID,
|
|
Code: sc.Code,
|
|
ResidenceID: sc.ResidenceID,
|
|
CreatedByID: sc.CreatedByID,
|
|
IsActive: sc.IsActive,
|
|
ExpiresAt: sc.ExpiresAt,
|
|
CreatedAt: sc.CreatedAt,
|
|
}
|
|
}
|