Add multi-image support for task completions and documents

- Add TaskCompletionImage and DocumentImage models with one-to-many relationships
- Update admin panel to display images for completions and documents
- Add image arrays to API request/response DTOs
- Update repositories with Preload("Images") for eager loading
- Fix seed SQL execution to use raw SQL instead of prepared statements
- Fix table names in seed file (admin_users, push_notifications_*)
- Add comprehensive seed test data with 34 completion images and 24 document images
- Add subscription limitations admin feature with toggle
- Update admin sidebar with limitations link

🤖 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-28 11:07:51 -06:00
parent 3cd222c048
commit 5e95dcd015
31 changed files with 2595 additions and 320 deletions

View File

@@ -25,6 +25,7 @@ type CreateDocumentRequest struct {
SerialNumber string `json:"serial_number" binding:"max=100"`
ModelNumber string `json:"model_number" binding:"max=100"`
TaskID *uint `json:"task_id"`
ImageURLs []string `json:"image_urls"` // Multiple image URLs
}
// UpdateDocumentRequest represents the request to update a document

View File

@@ -88,5 +88,12 @@ type CreateTaskCompletionRequest struct {
CompletedAt *time.Time `json:"completed_at"` // Defaults to now
Notes string `json:"notes"`
ActualCost *decimal.Decimal `json:"actual_cost"`
PhotoURL string `json:"photo_url"`
Rating *int `json:"rating"` // 1-5 star rating
ImageURLs []string `json:"image_urls"` // Multiple image URLs
}
// CompletionImageInput represents an image to add to a completion
type CompletionImageInput struct {
ImageURL string `json:"image_url" binding:"required"`
Caption string `json:"caption"`
}

View File

@@ -16,6 +16,13 @@ type DocumentUserResponse struct {
LastName string `json:"last_name"`
}
// DocumentImageResponse represents an image in a document
type DocumentImageResponse struct {
ID uint `json:"id"`
ImageURL string `json:"image_url"`
Caption string `json:"caption"`
}
// DocumentResponse represents a document in the API response
type DocumentResponse struct {
ID uint `json:"id"`
@@ -36,10 +43,11 @@ type DocumentResponse struct {
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"`
TaskID *uint `json:"task_id"`
IsActive bool `json:"is_active"`
Images []DocumentImageResponse `json:"images"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Note: Pagination removed - list endpoints now return arrays directly
@@ -81,6 +89,7 @@ func NewDocumentResponse(d *models.Document) DocumentResponse {
ModelNumber: d.ModelNumber,
TaskID: d.TaskID,
IsActive: d.IsActive,
Images: make([]DocumentImageResponse, 0),
CreatedAt: d.CreatedAt,
UpdatedAt: d.UpdatedAt,
}
@@ -89,6 +98,15 @@ func NewDocumentResponse(d *models.Document) DocumentResponse {
resp.CreatedBy = NewDocumentUserResponse(&d.CreatedBy)
}
// Convert images
for _, img := range d.Images {
resp.Images = append(resp.Images, DocumentImageResponse{
ID: img.ID,
ImageURL: img.ImageURL,
Caption: img.Caption,
})
}
return resp
}

View File

@@ -54,16 +54,24 @@ type TaskUserResponse struct {
LastName string `json:"last_name"`
}
// TaskCompletionImageResponse represents a completion image
type TaskCompletionImageResponse struct {
ID uint `json:"id"`
ImageURL string `json:"image_url"`
Caption string `json:"caption"`
}
// 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"`
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"`
Rating *int `json:"rating"`
Images []TaskCompletionImageResponse `json:"images"`
CreatedAt time.Time `json:"created_at"`
}
// TaskResponse represents a task in the API response
@@ -198,12 +206,21 @@ func NewTaskCompletionResponse(c *models.TaskCompletion) TaskCompletionResponse
CompletedAt: c.CompletedAt,
Notes: c.Notes,
ActualCost: c.ActualCost,
PhotoURL: c.PhotoURL,
Rating: c.Rating,
Images: make([]TaskCompletionImageResponse, 0),
CreatedAt: c.CreatedAt,
}
if c.CompletedBy.ID != 0 {
resp.CompletedBy = NewTaskUserResponse(&c.CompletedBy)
}
// Convert images
for _, img := range c.Images {
resp.Images = append(resp.Images, TaskCompletionImageResponse{
ID: img.ID,
ImageURL: img.ImageURL,
Caption: img.Caption,
})
}
return resp
}