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:
Trey t
2025-11-27 23:36:20 -06:00
parent 2817deee3c
commit 469f21a833
50 changed files with 6795 additions and 582 deletions

View File

@@ -75,8 +75,8 @@ func (s *TaskService) GetTask(taskID, userID uint) (*responses.TaskResponse, err
return &resp, nil
}
// ListTasks lists all tasks accessible to a user
func (s *TaskService) ListTasks(userID uint) (*responses.TaskListResponse, error) {
// ListTasks lists all tasks accessible to a user as a kanban board
func (s *TaskService) ListTasks(userID uint) (*responses.KanbanBoardResponse, error) {
// Get all residence IDs accessible to user
residences, err := s.residenceRepo.FindByUser(userID)
if err != nil {
@@ -89,15 +89,21 @@ func (s *TaskService) ListTasks(userID uint) (*responses.TaskListResponse, error
}
if len(residenceIDs) == 0 {
return &responses.TaskListResponse{Count: 0, Results: []responses.TaskResponse{}}, nil
// Return empty kanban board
return &responses.KanbanBoardResponse{
Columns: []responses.KanbanColumnResponse{},
DaysThreshold: 30,
ResidenceID: "all",
}, nil
}
tasks, err := s.taskRepo.FindByUser(userID, residenceIDs)
// Get kanban data aggregated across all residences
board, err := s.taskRepo.GetKanbanDataForMultipleResidences(residenceIDs, 30)
if err != nil {
return nil, err
}
resp := responses.NewTaskListResponse(tasks)
resp := responses.NewKanbanBoardResponseForAll(board)
return &resp, nil
}
@@ -146,7 +152,7 @@ func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint) (
StatusID: req.StatusID,
FrequencyID: req.FrequencyID,
AssignedToID: req.AssignedToID,
DueDate: req.DueDate,
DueDate: req.DueDate.ToTimePtr(),
EstimatedCost: req.EstimatedCost,
ContractorID: req.ContractorID,
}
@@ -207,7 +213,7 @@ func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRe
task.AssignedToID = req.AssignedToID
}
if req.DueDate != nil {
task.DueDate = req.DueDate
task.DueDate = req.DueDate.ToTimePtr()
}
if req.EstimatedCost != nil {
task.EstimatedCost = req.EstimatedCost
@@ -583,7 +589,7 @@ func (s *TaskService) GetCompletion(completionID, userID uint) (*responses.TaskC
}
// ListCompletions lists all task completions for a user
func (s *TaskService) ListCompletions(userID uint) (*responses.TaskCompletionListResponse, error) {
func (s *TaskService) ListCompletions(userID uint) ([]responses.TaskCompletionResponse, error) {
// Get all residence IDs
residences, err := s.residenceRepo.FindByUser(userID)
if err != nil {
@@ -596,7 +602,7 @@ func (s *TaskService) ListCompletions(userID uint) (*responses.TaskCompletionLis
}
if len(residenceIDs) == 0 {
return &responses.TaskCompletionListResponse{Count: 0, Results: []responses.TaskCompletionResponse{}}, nil
return []responses.TaskCompletionResponse{}, nil
}
completions, err := s.taskRepo.FindCompletionsByUser(userID, residenceIDs)
@@ -604,8 +610,7 @@ func (s *TaskService) ListCompletions(userID uint) (*responses.TaskCompletionLis
return nil, err
}
resp := responses.NewTaskCompletionListResponse(completions)
return &resp, nil
return responses.NewTaskCompletionListResponse(completions), nil
}
// DeleteCompletion deletes a task completion
@@ -630,6 +635,35 @@ func (s *TaskService) DeleteCompletion(completionID, userID uint) error {
return s.taskRepo.DeleteCompletion(completionID)
}
// GetCompletionsByTask gets all completions for a specific task
func (s *TaskService) GetCompletionsByTask(taskID, userID uint) ([]responses.TaskCompletionResponse, error) {
// Get the task to check access
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrTaskNotFound
}
return nil, err
}
// Check access via residence
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
if err != nil {
return nil, err
}
if !hasAccess {
return nil, ErrTaskAccessDenied
}
// Get completions for the task
completions, err := s.taskRepo.FindCompletionsByTask(taskID)
if err != nil {
return nil, err
}
return responses.NewTaskCompletionListResponse(completions), nil
}
// === Lookups ===
// GetCategories returns all task categories