Initial commit: MyCrib API in Go

Complete rewrite of Django REST API to Go with:
- Gin web framework for HTTP routing
- GORM for database operations
- GoAdmin for admin panel
- Gorush integration for push notifications
- Redis for caching and job queues

Features implemented:
- User authentication (login, register, logout, password reset)
- Residence management (CRUD, sharing, share codes)
- Task management (CRUD, kanban board, completions)
- Contractor management (CRUD, specialties)
- Document management (CRUD, warranties)
- Notifications (preferences, push notifications)
- Subscription management (tiers, limits)

Infrastructure:
- Docker Compose for local development
- Database migrations and seed data
- Admin panel for data management

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-11-26 20:07:16 -06:00
commit 1f12f3f62a
78 changed files with 13821 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
package requests
// LoginRequest represents the login request body
type LoginRequest struct {
Username string `json:"username" binding:"required_without=Email"`
Email string `json:"email" binding:"required_without=Username,omitempty,email"`
Password string `json:"password" binding:"required,min=1"`
}
// RegisterRequest represents the registration request body
type RegisterRequest struct {
Username string `json:"username" binding:"required,min=3,max=150"`
Email string `json:"email" binding:"required,email,max=254"`
Password string `json:"password" binding:"required,min=8"`
FirstName string `json:"first_name" binding:"max=150"`
LastName string `json:"last_name" binding:"max=150"`
}
// VerifyEmailRequest represents the email verification request body
type VerifyEmailRequest struct {
Code string `json:"code" binding:"required,len=6"`
}
// ForgotPasswordRequest represents the forgot password request body
type ForgotPasswordRequest struct {
Email string `json:"email" binding:"required,email"`
}
// VerifyResetCodeRequest represents the verify reset code request body
type VerifyResetCodeRequest struct {
Email string `json:"email" binding:"required,email"`
Code string `json:"code" binding:"required,len=6"`
}
// ResetPasswordRequest represents the reset password request body
type ResetPasswordRequest struct {
ResetToken string `json:"reset_token" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=8"`
}
// UpdateProfileRequest represents the profile update request body
type UpdateProfileRequest struct {
Email *string `json:"email" binding:"omitempty,email,max=254"`
FirstName *string `json:"first_name" binding:"omitempty,max=150"`
LastName *string `json:"last_name" binding:"omitempty,max=150"`
}
// ResendVerificationRequest represents the resend verification email request
type ResendVerificationRequest struct {
// No body needed - uses authenticated user's email
}

View File

@@ -0,0 +1,36 @@
package requests
// CreateContractorRequest represents the request to create a contractor
type CreateContractorRequest struct {
ResidenceID uint `json:"residence_id" binding:"required"`
Name string `json:"name" binding:"required,min=1,max=200"`
Company string `json:"company" binding:"max=200"`
Phone string `json:"phone" binding:"max=20"`
Email string `json:"email" binding:"omitempty,email,max=254"`
Website string `json:"website" binding:"max=200"`
Notes string `json:"notes"`
StreetAddress string `json:"street_address" binding:"max=255"`
City string `json:"city" binding:"max=100"`
StateProvince string `json:"state_province" binding:"max=100"`
PostalCode string `json:"postal_code" binding:"max=20"`
SpecialtyIDs []uint `json:"specialty_ids"`
Rating *float64 `json:"rating"`
IsFavorite *bool `json:"is_favorite"`
}
// UpdateContractorRequest represents the request to update a contractor
type UpdateContractorRequest struct {
Name *string `json:"name" binding:"omitempty,min=1,max=200"`
Company *string `json:"company" binding:"omitempty,max=200"`
Phone *string `json:"phone" binding:"omitempty,max=20"`
Email *string `json:"email" binding:"omitempty,email,max=254"`
Website *string `json:"website" binding:"omitempty,max=200"`
Notes *string `json:"notes"`
StreetAddress *string `json:"street_address" binding:"omitempty,max=255"`
City *string `json:"city" binding:"omitempty,max=100"`
StateProvince *string `json:"state_province" binding:"omitempty,max=100"`
PostalCode *string `json:"postal_code" binding:"omitempty,max=20"`
SpecialtyIDs []uint `json:"specialty_ids"`
Rating *float64 `json:"rating"`
IsFavorite *bool `json:"is_favorite"`
}

View File

@@ -0,0 +1,46 @@
package requests
import (
"time"
"github.com/shopspring/decimal"
"github.com/treytartt/mycrib-api/internal/models"
)
// CreateDocumentRequest represents the request to create a document
type CreateDocumentRequest struct {
ResidenceID uint `json:"residence_id" binding:"required"`
Title string `json:"title" binding:"required,min=1,max=200"`
Description string `json:"description"`
DocumentType models.DocumentType `json:"document_type"`
FileURL string `json:"file_url" binding:"max=500"`
FileName string `json:"file_name" binding:"max=255"`
FileSize *int64 `json:"file_size"`
MimeType string `json:"mime_type" binding:"max=100"`
PurchaseDate *time.Time `json:"purchase_date"`
ExpiryDate *time.Time `json:"expiry_date"`
PurchasePrice *decimal.Decimal `json:"purchase_price"`
Vendor string `json:"vendor" binding:"max=200"`
SerialNumber string `json:"serial_number" binding:"max=100"`
ModelNumber string `json:"model_number" binding:"max=100"`
TaskID *uint `json:"task_id"`
}
// UpdateDocumentRequest represents the request to update a document
type UpdateDocumentRequest struct {
Title *string `json:"title" binding:"omitempty,min=1,max=200"`
Description *string `json:"description"`
DocumentType *models.DocumentType `json:"document_type"`
FileURL *string `json:"file_url" binding:"omitempty,max=500"`
FileName *string `json:"file_name" binding:"omitempty,max=255"`
FileSize *int64 `json:"file_size"`
MimeType *string `json:"mime_type" binding:"omitempty,max=100"`
PurchaseDate *time.Time `json:"purchase_date"`
ExpiryDate *time.Time `json:"expiry_date"`
PurchasePrice *decimal.Decimal `json:"purchase_price"`
Vendor *string `json:"vendor" binding:"omitempty,max=200"`
SerialNumber *string `json:"serial_number" binding:"omitempty,max=100"`
ModelNumber *string `json:"model_number" binding:"omitempty,max=100"`
TaskID *uint `json:"task_id"`
}

View File

@@ -0,0 +1,59 @@
package requests
import (
"time"
"github.com/shopspring/decimal"
)
// CreateResidenceRequest represents the request to create a residence
type CreateResidenceRequest struct {
Name string `json:"name" binding:"required,min=1,max=200"`
PropertyTypeID *uint `json:"property_type_id"`
StreetAddress string `json:"street_address" binding:"max=255"`
ApartmentUnit string `json:"apartment_unit" binding:"max=50"`
City string `json:"city" binding:"max=100"`
StateProvince string `json:"state_province" binding:"max=100"`
PostalCode string `json:"postal_code" binding:"max=20"`
Country string `json:"country" binding:"max=100"`
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"`
IsPrimary *bool `json:"is_primary"`
}
// UpdateResidenceRequest represents the request to update a residence
type UpdateResidenceRequest struct {
Name *string `json:"name" binding:"omitempty,min=1,max=200"`
PropertyTypeID *uint `json:"property_type_id"`
StreetAddress *string `json:"street_address" binding:"omitempty,max=255"`
ApartmentUnit *string `json:"apartment_unit" binding:"omitempty,max=50"`
City *string `json:"city" binding:"omitempty,max=100"`
StateProvince *string `json:"state_province" binding:"omitempty,max=100"`
PostalCode *string `json:"postal_code" binding:"omitempty,max=20"`
Country *string `json:"country" binding:"omitempty,max=100"`
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"`
IsPrimary *bool `json:"is_primary"`
}
// JoinWithCodeRequest represents the request to join a residence via share code
type JoinWithCodeRequest struct {
Code string `json:"code" binding:"required,len=6"`
}
// GenerateShareCodeRequest represents the request to generate a share code
type GenerateShareCodeRequest struct {
ExpiresInHours int `json:"expires_in_hours"` // Default: 24 hours
}

View File

@@ -0,0 +1,46 @@
package requests
import (
"time"
"github.com/shopspring/decimal"
)
// CreateTaskRequest represents the request to create a task
type CreateTaskRequest struct {
ResidenceID uint `json:"residence_id" binding:"required"`
Title string `json:"title" binding:"required,min=1,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 *time.Time `json:"due_date"`
EstimatedCost *decimal.Decimal `json:"estimated_cost"`
ContractorID *uint `json:"contractor_id"`
}
// UpdateTaskRequest represents the request to update a task
type UpdateTaskRequest struct {
Title *string `json:"title" binding:"omitempty,min=1,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 *time.Time `json:"due_date"`
EstimatedCost *decimal.Decimal `json:"estimated_cost"`
ActualCost *decimal.Decimal `json:"actual_cost"`
ContractorID *uint `json:"contractor_id"`
}
// CreateTaskCompletionRequest represents the request to create a task completion
type CreateTaskCompletionRequest struct {
TaskID uint `json:"task_id" binding:"required"`
CompletedAt *time.Time `json:"completed_at"` // Defaults to now
Notes string `json:"notes"`
ActualCost *decimal.Decimal `json:"actual_cost"`
PhotoURL string `json:"photo_url"`
}

View File

@@ -0,0 +1,151 @@
package responses
import (
"time"
"github.com/treytartt/mycrib-api/internal/models"
)
// UserResponse represents a user in API responses
type UserResponse struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
IsActive bool `json:"is_active"`
DateJoined time.Time `json:"date_joined"`
LastLogin *time.Time `json:"last_login,omitempty"`
}
// UserProfileResponse represents a user profile in API responses
type UserProfileResponse struct {
ID uint `json:"id"`
UserID uint `json:"user_id"`
Verified bool `json:"verified"`
Bio string `json:"bio"`
PhoneNumber string `json:"phone_number"`
DateOfBirth *time.Time `json:"date_of_birth,omitempty"`
ProfilePicture string `json:"profile_picture"`
}
// LoginResponse represents the login response
type LoginResponse struct {
Token string `json:"token"`
User UserResponse `json:"user"`
}
// RegisterResponse represents the registration response
type RegisterResponse struct {
Token string `json:"token"`
User UserResponse `json:"user"`
Message string `json:"message"`
}
// CurrentUserResponse represents the /auth/me/ response
type CurrentUserResponse struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
IsActive bool `json:"is_active"`
DateJoined time.Time `json:"date_joined"`
LastLogin *time.Time `json:"last_login,omitempty"`
Profile *UserProfileResponse `json:"profile,omitempty"`
}
// VerifyEmailResponse represents the email verification response
type VerifyEmailResponse struct {
Message string `json:"message"`
Verified bool `json:"verified"`
}
// ForgotPasswordResponse represents the forgot password response
type ForgotPasswordResponse struct {
Message string `json:"message"`
}
// VerifyResetCodeResponse represents the verify reset code response
type VerifyResetCodeResponse struct {
Message string `json:"message"`
ResetToken string `json:"reset_token"`
}
// ResetPasswordResponse represents the reset password response
type ResetPasswordResponse struct {
Message string `json:"message"`
}
// MessageResponse represents a simple message response
type MessageResponse struct {
Message string `json:"message"`
}
// ErrorResponse represents an error response
type ErrorResponse struct {
Error string `json:"error"`
Details map[string]string `json:"details,omitempty"`
}
// NewUserResponse creates a UserResponse from a User model
func NewUserResponse(user *models.User) UserResponse {
return UserResponse{
ID: user.ID,
Username: user.Username,
Email: user.Email,
FirstName: user.FirstName,
LastName: user.LastName,
IsActive: user.IsActive,
DateJoined: user.DateJoined,
LastLogin: user.LastLogin,
}
}
// NewUserProfileResponse creates a UserProfileResponse from a UserProfile model
func NewUserProfileResponse(profile *models.UserProfile) *UserProfileResponse {
if profile == nil {
return nil
}
return &UserProfileResponse{
ID: profile.ID,
UserID: profile.UserID,
Verified: profile.Verified,
Bio: profile.Bio,
PhoneNumber: profile.PhoneNumber,
DateOfBirth: profile.DateOfBirth,
ProfilePicture: profile.ProfilePicture,
}
}
// NewCurrentUserResponse creates a CurrentUserResponse from a User model
func NewCurrentUserResponse(user *models.User) CurrentUserResponse {
return CurrentUserResponse{
ID: user.ID,
Username: user.Username,
Email: user.Email,
FirstName: user.FirstName,
LastName: user.LastName,
IsActive: user.IsActive,
DateJoined: user.DateJoined,
LastLogin: user.LastLogin,
Profile: NewUserProfileResponse(user.Profile),
}
}
// NewLoginResponse creates a LoginResponse
func NewLoginResponse(token string, user *models.User) LoginResponse {
return LoginResponse{
Token: token,
User: NewUserResponse(user),
}
}
// NewRegisterResponse creates a RegisterResponse
func NewRegisterResponse(token string, user *models.User) RegisterResponse {
return RegisterResponse{
Token: token,
User: NewUserResponse(user),
Message: "Registration successful. Please check your email to verify your account.",
}
}

View File

@@ -0,0 +1,139 @@
package responses
import (
"time"
"github.com/treytartt/mycrib-api/internal/models"
)
// ContractorSpecialtyResponse represents a contractor specialty
type ContractorSpecialtyResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Icon string `json:"icon"`
DisplayOrder int `json:"display_order"`
}
// ContractorUserResponse represents a user in contractor context
type ContractorUserResponse struct {
ID uint `json:"id"`
Username string `json:"username"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
// ContractorResponse represents a contractor in the API response
type ContractorResponse struct {
ID uint `json:"id"`
ResidenceID uint `json:"residence_id"`
CreatedByID uint `json:"created_by_id"`
CreatedBy *ContractorUserResponse `json:"created_by,omitempty"`
Name string `json:"name"`
Company string `json:"company"`
Phone string `json:"phone"`
Email string `json:"email"`
Website string `json:"website"`
Notes string `json:"notes"`
StreetAddress string `json:"street_address"`
City string `json:"city"`
StateProvince string `json:"state_province"`
PostalCode string `json:"postal_code"`
Specialties []ContractorSpecialtyResponse `json:"specialties"`
Rating *float64 `json:"rating"`
IsFavorite bool `json:"is_favorite"`
IsActive bool `json:"is_active"`
TaskCount int `json:"task_count,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ContractorListResponse represents a paginated list of contractors
type ContractorListResponse struct {
Count int `json:"count"`
Next *string `json:"next"`
Previous *string `json:"previous"`
Results []ContractorResponse `json:"results"`
}
// ToggleFavoriteResponse represents the response after toggling favorite
type ToggleFavoriteResponse struct {
Message string `json:"message"`
IsFavorite bool `json:"is_favorite"`
}
// === Factory Functions ===
// NewContractorSpecialtyResponse creates a ContractorSpecialtyResponse from a model
func NewContractorSpecialtyResponse(s *models.ContractorSpecialty) ContractorSpecialtyResponse {
return ContractorSpecialtyResponse{
ID: s.ID,
Name: s.Name,
Description: s.Description,
Icon: s.Icon,
DisplayOrder: s.DisplayOrder,
}
}
// NewContractorUserResponse creates a ContractorUserResponse from a User model
func NewContractorUserResponse(u *models.User) *ContractorUserResponse {
if u == nil {
return nil
}
return &ContractorUserResponse{
ID: u.ID,
Username: u.Username,
FirstName: u.FirstName,
LastName: u.LastName,
}
}
// NewContractorResponse creates a ContractorResponse from a Contractor model
func NewContractorResponse(c *models.Contractor) ContractorResponse {
resp := ContractorResponse{
ID: c.ID,
ResidenceID: c.ResidenceID,
CreatedByID: c.CreatedByID,
Name: c.Name,
Company: c.Company,
Phone: c.Phone,
Email: c.Email,
Website: c.Website,
Notes: c.Notes,
StreetAddress: c.StreetAddress,
City: c.City,
StateProvince: c.StateProvince,
PostalCode: c.PostalCode,
Rating: c.Rating,
IsFavorite: c.IsFavorite,
IsActive: c.IsActive,
TaskCount: len(c.Tasks),
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
}
if c.CreatedBy.ID != 0 {
resp.CreatedBy = NewContractorUserResponse(&c.CreatedBy)
}
resp.Specialties = make([]ContractorSpecialtyResponse, len(c.Specialties))
for i, s := range c.Specialties {
resp.Specialties[i] = NewContractorSpecialtyResponse(&s)
}
return resp
}
// NewContractorListResponse creates a ContractorListResponse from a slice of contractors
func NewContractorListResponse(contractors []models.Contractor) ContractorListResponse {
results := make([]ContractorResponse, len(contractors))
for i, c := range contractors {
results[i] = NewContractorResponse(&c)
}
return ContractorListResponse{
Count: len(contractors),
Next: nil,
Previous: nil,
Results: results,
}
}

View File

@@ -0,0 +1,111 @@
package responses
import (
"time"
"github.com/shopspring/decimal"
"github.com/treytartt/mycrib-api/internal/models"
)
// DocumentUserResponse represents a user in document context
type DocumentUserResponse struct {
ID uint `json:"id"`
Username string `json:"username"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
// DocumentResponse represents a document in the API response
type DocumentResponse struct {
ID uint `json:"id"`
ResidenceID uint `json:"residence_id"`
CreatedByID uint `json:"created_by_id"`
CreatedBy *DocumentUserResponse `json:"created_by,omitempty"`
Title string `json:"title"`
Description string `json:"description"`
DocumentType models.DocumentType `json:"document_type"`
FileURL string `json:"file_url"`
FileName string `json:"file_name"`
FileSize *int64 `json:"file_size"`
MimeType string `json:"mime_type"`
PurchaseDate *time.Time `json:"purchase_date"`
ExpiryDate *time.Time `json:"expiry_date"`
PurchasePrice *decimal.Decimal `json:"purchase_price"`
Vendor string `json:"vendor"`
SerialNumber string `json:"serial_number"`
ModelNumber string `json:"model_number"`
TaskID *uint `json:"task_id"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// DocumentListResponse represents a paginated list of documents
type DocumentListResponse struct {
Count int `json:"count"`
Next *string `json:"next"`
Previous *string `json:"previous"`
Results []DocumentResponse `json:"results"`
}
// === Factory Functions ===
// NewDocumentUserResponse creates a DocumentUserResponse from a User model
func NewDocumentUserResponse(u *models.User) *DocumentUserResponse {
if u == nil {
return nil
}
return &DocumentUserResponse{
ID: u.ID,
Username: u.Username,
FirstName: u.FirstName,
LastName: u.LastName,
}
}
// NewDocumentResponse creates a DocumentResponse from a Document model
func NewDocumentResponse(d *models.Document) DocumentResponse {
resp := DocumentResponse{
ID: d.ID,
ResidenceID: d.ResidenceID,
CreatedByID: d.CreatedByID,
Title: d.Title,
Description: d.Description,
DocumentType: d.DocumentType,
FileURL: d.FileURL,
FileName: d.FileName,
FileSize: d.FileSize,
MimeType: d.MimeType,
PurchaseDate: d.PurchaseDate,
ExpiryDate: d.ExpiryDate,
PurchasePrice: d.PurchasePrice,
Vendor: d.Vendor,
SerialNumber: d.SerialNumber,
ModelNumber: d.ModelNumber,
TaskID: d.TaskID,
IsActive: d.IsActive,
CreatedAt: d.CreatedAt,
UpdatedAt: d.UpdatedAt,
}
if d.CreatedBy.ID != 0 {
resp.CreatedBy = NewDocumentUserResponse(&d.CreatedBy)
}
return resp
}
// NewDocumentListResponse creates a DocumentListResponse from a slice of documents
func NewDocumentListResponse(documents []models.Document) DocumentListResponse {
results := make([]DocumentResponse, len(documents))
for i, d := range documents {
results[i] = NewDocumentResponse(&d)
}
return DocumentListResponse{
Count: len(documents),
Next: nil,
Previous: nil,
Results: results,
}
}

View File

@@ -0,0 +1,189 @@
package responses
import (
"time"
"github.com/shopspring/decimal"
"github.com/treytartt/mycrib-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"`
IsPrimary bool `json:"is_primary"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ResidenceListResponse represents the paginated list of residences
type ResidenceListResponse struct {
Count int `json:"count"`
Next *string `json:"next"`
Previous *string `json:"previous"`
Results []ResidenceResponse `json:"results"`
}
// 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"`
}
// GenerateShareCodeResponse represents the response after generating a share code
type GenerateShareCodeResponse struct {
Message string `json:"message"`
ShareCode ShareCodeResponse `json:"share_code"`
}
// === 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,
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 paginated list response
func NewResidenceListResponse(residences []models.Residence) ResidenceListResponse {
results := make([]ResidenceResponse, len(residences))
for i, r := range residences {
results[i] = NewResidenceResponse(&r)
}
return ResidenceListResponse{
Count: len(residences),
Next: nil, // Pagination not implemented yet
Previous: nil,
Results: 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,
}
}

View File

@@ -0,0 +1,324 @@
package responses
import (
"fmt"
"time"
"github.com/shopspring/decimal"
"github.com/treytartt/mycrib-api/internal/models"
)
// TaskCategoryResponse represents a task category
type TaskCategoryResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Icon string `json:"icon"`
Color string `json:"color"`
DisplayOrder int `json:"display_order"`
}
// TaskPriorityResponse represents a task priority
type TaskPriorityResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Level int `json:"level"`
Color string `json:"color"`
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"`
Name string `json:"name"`
Days *int `json:"days"`
DisplayOrder int `json:"display_order"`
}
// TaskUserResponse represents a user in task context
type TaskUserResponse struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
// TaskCompletionResponse represents a task completion
type TaskCompletionResponse struct {
ID uint `json:"id"`
TaskID uint `json:"task_id"`
CompletedBy *TaskUserResponse `json:"completed_by,omitempty"`
CompletedAt time.Time `json:"completed_at"`
Notes string `json:"notes"`
ActualCost *decimal.Decimal `json:"actual_cost"`
PhotoURL string `json:"photo_url"`
CreatedAt time.Time `json:"created_at"`
}
// TaskResponse represents a task in the API response
type TaskResponse struct {
ID uint `json:"id"`
ResidenceID uint `json:"residence_id"`
CreatedByID uint `json:"created_by_id"`
CreatedBy *TaskUserResponse `json:"created_by,omitempty"`
AssignedToID *uint `json:"assigned_to_id"`
AssignedTo *TaskUserResponse `json:"assigned_to,omitempty"`
Title string `json:"title"`
Description string `json:"description"`
CategoryID *uint `json:"category_id"`
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"`
DueDate *time.Time `json:"due_date"`
EstimatedCost *decimal.Decimal `json:"estimated_cost"`
ActualCost *decimal.Decimal `json:"actual_cost"`
ContractorID *uint `json:"contractor_id"`
IsCancelled bool `json:"is_cancelled"`
IsArchived bool `json:"is_archived"`
ParentTaskID *uint `json:"parent_task_id"`
Completions []TaskCompletionResponse `json:"completions,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TaskListResponse represents a paginated list of tasks
type TaskListResponse struct {
Count int `json:"count"`
Next *string `json:"next"`
Previous *string `json:"previous"`
Results []TaskResponse `json:"results"`
}
// KanbanColumnResponse represents a kanban column
type KanbanColumnResponse struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
ButtonTypes []string `json:"button_types"`
Icons map[string]string `json:"icons"`
Color string `json:"color"`
Tasks []TaskResponse `json:"tasks"`
Count int `json:"count"`
}
// KanbanBoardResponse represents the kanban board
type KanbanBoardResponse struct {
Columns []KanbanColumnResponse `json:"columns"`
DaysThreshold int `json:"days_threshold"`
ResidenceID string `json:"residence_id"`
}
// TaskCompletionListResponse represents a list of completions
type TaskCompletionListResponse struct {
Count int `json:"count"`
Next *string `json:"next"`
Previous *string `json:"previous"`
Results []TaskCompletionResponse `json:"results"`
}
// === Factory Functions ===
// NewTaskCategoryResponse creates a TaskCategoryResponse from a model
func NewTaskCategoryResponse(c *models.TaskCategory) *TaskCategoryResponse {
if c == nil {
return nil
}
return &TaskCategoryResponse{
ID: c.ID,
Name: c.Name,
Description: c.Description,
Icon: c.Icon,
Color: c.Color,
DisplayOrder: c.DisplayOrder,
}
}
// NewTaskPriorityResponse creates a TaskPriorityResponse from a model
func NewTaskPriorityResponse(p *models.TaskPriority) *TaskPriorityResponse {
if p == nil {
return nil
}
return &TaskPriorityResponse{
ID: p.ID,
Name: p.Name,
Level: p.Level,
Color: p.Color,
DisplayOrder: p.DisplayOrder,
}
}
// 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 {
return nil
}
return &TaskFrequencyResponse{
ID: f.ID,
Name: f.Name,
Days: f.Days,
DisplayOrder: f.DisplayOrder,
}
}
// NewTaskUserResponse creates a TaskUserResponse from a User model
func NewTaskUserResponse(u *models.User) *TaskUserResponse {
if u == nil {
return nil
}
return &TaskUserResponse{
ID: u.ID,
Username: u.Username,
Email: u.Email,
FirstName: u.FirstName,
LastName: u.LastName,
}
}
// NewTaskCompletionResponse creates a TaskCompletionResponse from a model
func NewTaskCompletionResponse(c *models.TaskCompletion) TaskCompletionResponse {
resp := TaskCompletionResponse{
ID: c.ID,
TaskID: c.TaskID,
CompletedAt: c.CompletedAt,
Notes: c.Notes,
ActualCost: c.ActualCost,
PhotoURL: c.PhotoURL,
CreatedAt: c.CreatedAt,
}
if c.CompletedBy.ID != 0 {
resp.CompletedBy = NewTaskUserResponse(&c.CompletedBy)
}
return resp
}
// NewTaskResponse creates a TaskResponse from a Task model
func NewTaskResponse(t *models.Task) TaskResponse {
resp := TaskResponse{
ID: t.ID,
ResidenceID: t.ResidenceID,
CreatedByID: t.CreatedByID,
Title: t.Title,
Description: t.Description,
CategoryID: t.CategoryID,
PriorityID: t.PriorityID,
StatusID: t.StatusID,
FrequencyID: t.FrequencyID,
AssignedToID: t.AssignedToID,
DueDate: t.DueDate,
EstimatedCost: t.EstimatedCost,
ActualCost: t.ActualCost,
ContractorID: t.ContractorID,
IsCancelled: t.IsCancelled,
IsArchived: t.IsArchived,
ParentTaskID: t.ParentTaskID,
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
}
if t.CreatedBy.ID != 0 {
resp.CreatedBy = NewTaskUserResponse(&t.CreatedBy)
}
if t.AssignedTo != nil {
resp.AssignedTo = NewTaskUserResponse(t.AssignedTo)
}
if t.Category != nil {
resp.Category = NewTaskCategoryResponse(t.Category)
}
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)
}
resp.Completions = make([]TaskCompletionResponse, len(t.Completions))
for i, c := range t.Completions {
resp.Completions[i] = NewTaskCompletionResponse(&c)
}
return resp
}
// NewTaskListResponse creates a TaskListResponse from a slice of tasks
func NewTaskListResponse(tasks []models.Task) TaskListResponse {
results := make([]TaskResponse, len(tasks))
for i, t := range tasks {
results[i] = NewTaskResponse(&t)
}
return TaskListResponse{
Count: len(tasks),
Next: nil,
Previous: nil,
Results: results,
}
}
// NewKanbanBoardResponse creates a KanbanBoardResponse from a KanbanBoard model
func NewKanbanBoardResponse(board *models.KanbanBoard, residenceID uint) KanbanBoardResponse {
columns := make([]KanbanColumnResponse, len(board.Columns))
for i, col := range board.Columns {
tasks := make([]TaskResponse, len(col.Tasks))
for j, t := range col.Tasks {
tasks[j] = NewTaskResponse(&t)
}
columns[i] = KanbanColumnResponse{
Name: col.Name,
DisplayName: col.DisplayName,
ButtonTypes: col.ButtonTypes,
Icons: col.Icons,
Color: col.Color,
Tasks: tasks,
Count: col.Count,
}
}
return KanbanBoardResponse{
Columns: columns,
DaysThreshold: board.DaysThreshold,
ResidenceID: fmt.Sprintf("%d", residenceID),
}
}
// NewTaskCompletionListResponse creates a TaskCompletionListResponse
func NewTaskCompletionListResponse(completions []models.TaskCompletion) TaskCompletionListResponse {
results := make([]TaskCompletionResponse, len(completions))
for i, c := range completions {
results[i] = NewTaskCompletionResponse(&c)
}
return TaskCompletionListResponse{
Count: len(completions),
Next: nil,
Previous: nil,
Results: results,
}
}