Add PDF reports, file uploads, admin auth, and comprehensive tests
Features: - PDF service for generating task reports with ReportLab-style formatting - Storage service for file uploads (local and S3-compatible) - Admin authentication middleware with JWT support - Admin user model and repository Infrastructure: - Updated Docker configuration for admin panel builds - Email service enhancements for task notifications - Updated router with admin and file upload routes - Environment configuration updates Tests: - Unit tests for handlers (auth, residence, task) - Unit tests for models (user, residence, task) - Unit tests for repositories (user, residence, task) - Unit tests for services (residence, task) - Integration test setup - Test utilities for mocking database and services Database: - Admin user seed data - Updated test data seeds 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -68,41 +68,36 @@ type TaskCompletionResponse struct {
|
||||
|
||||
// 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"`
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
// Note: Pagination removed - list endpoints now return arrays directly
|
||||
|
||||
// KanbanColumnResponse represents a kanban column
|
||||
type KanbanColumnResponse struct {
|
||||
@@ -122,13 +117,7 @@ type KanbanBoardResponse struct {
|
||||
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"`
|
||||
}
|
||||
// Note: TaskCompletionListResponse pagination removed - returns arrays directly
|
||||
|
||||
// === Factory Functions ===
|
||||
|
||||
@@ -222,25 +211,26 @@ func NewTaskCompletionResponse(c *models.TaskCompletion) TaskCompletionResponse
|
||||
// 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,
|
||||
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 {
|
||||
@@ -270,18 +260,13 @@ func NewTaskResponse(t *models.Task) TaskResponse {
|
||||
return resp
|
||||
}
|
||||
|
||||
// NewTaskListResponse creates a TaskListResponse from a slice of tasks
|
||||
func NewTaskListResponse(tasks []models.Task) TaskListResponse {
|
||||
// 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 TaskListResponse{
|
||||
Count: len(tasks),
|
||||
Next: nil,
|
||||
Previous: nil,
|
||||
Results: results,
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// NewKanbanBoardResponse creates a KanbanBoardResponse from a KanbanBoard model
|
||||
@@ -309,16 +294,36 @@ func NewKanbanBoardResponse(board *models.KanbanBoard, residenceID uint) KanbanB
|
||||
}
|
||||
}
|
||||
|
||||
// NewTaskCompletionListResponse creates a TaskCompletionListResponse
|
||||
func NewTaskCompletionListResponse(completions []models.TaskCompletion) TaskCompletionListResponse {
|
||||
// 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 TaskCompletionListResponse{
|
||||
Count: len(completions),
|
||||
Next: nil,
|
||||
Previous: nil,
|
||||
Results: results,
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user