- Add MediaHandler with token-based proxy endpoints for serving media: - GET /api/media/document/:id - GET /api/media/document-image/:id - GET /api/media/completion-image/:id - Add MediaURL fields to response DTOs for documents and task completions - Add FindImageByID and FindCompletionImageByID repository methods - Preload Completions.Images in all task queries for proper media URLs - Remove public /uploads static file serving for security - Verify residence access before serving any media files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
343 lines
11 KiB
Go
343 lines
11 KiB
Go
package responses
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/shopspring/decimal"
|
|
|
|
"github.com/treytartt/casera-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"`
|
|
}
|
|
|
|
// TaskCompletionImageResponse represents a completion image
|
|
type TaskCompletionImageResponse struct {
|
|
ID uint `json:"id"`
|
|
ImageURL string `json:"image_url"`
|
|
MediaURL string `json:"media_url"` // Authenticated endpoint: /api/media/completion-image/{id}
|
|
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"`
|
|
Rating *int `json:"rating"`
|
|
Images []TaskCompletionImageResponse `json:"images"`
|
|
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"`
|
|
CompletionCount int `json:"completion_count"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// Note: Pagination removed - list endpoints now return arrays directly
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// Note: TaskCompletionListResponse pagination removed - returns arrays directly
|
|
|
|
// === 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,
|
|
Rating: c.Rating,
|
|
Images: make([]TaskCompletionImageResponse, 0),
|
|
CreatedAt: c.CreatedAt,
|
|
}
|
|
if c.CompletedBy.ID != 0 {
|
|
resp.CompletedBy = NewTaskUserResponse(&c.CompletedBy)
|
|
}
|
|
// Convert images with authenticated media URLs
|
|
for _, img := range c.Images {
|
|
resp.Images = append(resp.Images, TaskCompletionImageResponse{
|
|
ID: img.ID,
|
|
ImageURL: img.ImageURL,
|
|
MediaURL: fmt.Sprintf("/api/media/completion-image/%d", img.ID), // Authenticated endpoint
|
|
Caption: img.Caption,
|
|
})
|
|
}
|
|
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,
|
|
CompletionCount: len(t.Completions),
|
|
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)
|
|
}
|
|
|
|
return resp
|
|
}
|
|
|
|
// NewTaskListResponse creates a list of task responses
|
|
func NewTaskListResponse(tasks []models.Task) []TaskResponse {
|
|
results := make([]TaskResponse, len(tasks))
|
|
for i, t := range tasks {
|
|
results[i] = NewTaskResponse(&t)
|
|
}
|
|
return 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),
|
|
}
|
|
}
|
|
|
|
// NewKanbanBoardResponseForAll creates a KanbanBoardResponse for all residences (no specific residence ID)
|
|
func NewKanbanBoardResponseForAll(board *models.KanbanBoard) 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: "all",
|
|
}
|
|
}
|
|
|
|
// NewTaskCompletionListResponse creates a list of task completion responses
|
|
func NewTaskCompletionListResponse(completions []models.TaskCompletion) []TaskCompletionResponse {
|
|
results := make([]TaskCompletionResponse, len(completions))
|
|
for i, c := range completions {
|
|
results[i] = NewTaskCompletionResponse(&c)
|
|
}
|
|
return results
|
|
}
|