Include TotalSummary in CRUD responses to eliminate redundant API calls

Backend changes:
- Add WithSummaryResponse wrappers for Task, TaskCompletion, and Residence CRUD
- Update services to return summary with all mutations (create, update, delete)
- Update handlers to pass through new response types
- Add getSummaryForUser helper for fetching summary in CRUD operations
- Wire ResidenceService into TaskService for summary access
- Add summary field to JoinResidenceResponse

This optimization eliminates the need for a separate getSummary() call after
every task/residence mutation, reducing network calls from 2 to 1.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-08 10:39:33 -06:00
parent f88409cfb4
commit 1a48fbfb20
9 changed files with 351 additions and 134 deletions

View File

@@ -27,6 +27,7 @@ var (
type TaskService struct {
taskRepo *repositories.TaskRepository
residenceRepo *repositories.ResidenceRepository
residenceService *ResidenceService
notificationService *NotificationService
emailService *EmailService
}
@@ -49,6 +50,23 @@ func (s *TaskService) SetEmailService(es *EmailService) {
s.emailService = es
}
// SetResidenceService sets the residence service (for getting summary in CRUD responses)
func (s *TaskService) SetResidenceService(rs *ResidenceService) {
s.residenceService = rs
}
// getSummaryForUser gets the total summary for a user (helper for CRUD responses)
func (s *TaskService) getSummaryForUser(userID uint) responses.TotalSummary {
if s.residenceService == nil {
return responses.TotalSummary{}
}
summary, err := s.residenceService.GetSummary(userID)
if err != nil || summary == nil {
return responses.TotalSummary{}
}
return *summary
}
// === Task CRUD ===
// GetTask gets a task by ID with access check
@@ -131,7 +149,7 @@ func (s *TaskService) GetTasksByResidence(residenceID, userID uint, daysThreshol
}
// CreateTask creates a new task
func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint) (*responses.TaskResponse, error) {
func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint) (*responses.TaskWithSummaryResponse, error) {
// Check residence access
hasAccess, err := s.residenceRepo.HasAccess(req.ResidenceID, userID)
if err != nil {
@@ -168,12 +186,14 @@ func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint) (
return nil, err
}
resp := responses.NewTaskResponse(task)
return &resp, nil
return &responses.TaskWithSummaryResponse{
Data: responses.NewTaskResponse(task),
Summary: s.getSummaryForUser(userID),
}, nil
}
// UpdateTask updates a task
func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRequest) (*responses.TaskResponse, error) {
func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRequest) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -242,36 +262,45 @@ func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRe
return nil, err
}
resp := responses.NewTaskResponse(task)
return &resp, nil
return &responses.TaskWithSummaryResponse{
Data: responses.NewTaskResponse(task),
Summary: s.getSummaryForUser(userID),
}, nil
}
// DeleteTask deletes a task
func (s *TaskService) DeleteTask(taskID, userID uint) error {
func (s *TaskService) DeleteTask(taskID, userID uint) (*responses.DeleteWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrTaskNotFound
return nil, ErrTaskNotFound
}
return err
return nil, err
}
// Check access
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
if err != nil {
return err
return nil, err
}
if !hasAccess {
return ErrTaskAccessDenied
return nil, ErrTaskAccessDenied
}
return s.taskRepo.Delete(taskID)
if err := s.taskRepo.Delete(taskID); err != nil {
return nil, err
}
return &responses.DeleteWithSummaryResponse{
Data: "task deleted",
Summary: s.getSummaryForUser(userID),
}, nil
}
// === Task Actions ===
// MarkInProgress marks a task as in progress
func (s *TaskService) MarkInProgress(taskID, userID uint) (*responses.TaskResponse, error) {
func (s *TaskService) MarkInProgress(taskID, userID uint) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -305,12 +334,14 @@ func (s *TaskService) MarkInProgress(taskID, userID uint) (*responses.TaskRespon
return nil, err
}
resp := responses.NewTaskResponse(task)
return &resp, nil
return &responses.TaskWithSummaryResponse{
Data: responses.NewTaskResponse(task),
Summary: s.getSummaryForUser(userID),
}, nil
}
// CancelTask cancels a task
func (s *TaskService) CancelTask(taskID, userID uint) (*responses.TaskResponse, error) {
func (s *TaskService) CancelTask(taskID, userID uint) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -342,12 +373,14 @@ func (s *TaskService) CancelTask(taskID, userID uint) (*responses.TaskResponse,
return nil, err
}
resp := responses.NewTaskResponse(task)
return &resp, nil
return &responses.TaskWithSummaryResponse{
Data: responses.NewTaskResponse(task),
Summary: s.getSummaryForUser(userID),
}, nil
}
// UncancelTask uncancels a task
func (s *TaskService) UncancelTask(taskID, userID uint) (*responses.TaskResponse, error) {
func (s *TaskService) UncancelTask(taskID, userID uint) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -375,12 +408,14 @@ func (s *TaskService) UncancelTask(taskID, userID uint) (*responses.TaskResponse
return nil, err
}
resp := responses.NewTaskResponse(task)
return &resp, nil
return &responses.TaskWithSummaryResponse{
Data: responses.NewTaskResponse(task),
Summary: s.getSummaryForUser(userID),
}, nil
}
// ArchiveTask archives a task
func (s *TaskService) ArchiveTask(taskID, userID uint) (*responses.TaskResponse, error) {
func (s *TaskService) ArchiveTask(taskID, userID uint) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -412,12 +447,14 @@ func (s *TaskService) ArchiveTask(taskID, userID uint) (*responses.TaskResponse,
return nil, err
}
resp := responses.NewTaskResponse(task)
return &resp, nil
return &responses.TaskWithSummaryResponse{
Data: responses.NewTaskResponse(task),
Summary: s.getSummaryForUser(userID),
}, nil
}
// UnarchiveTask unarchives a task
func (s *TaskService) UnarchiveTask(taskID, userID uint) (*responses.TaskResponse, error) {
func (s *TaskService) UnarchiveTask(taskID, userID uint) (*responses.TaskWithSummaryResponse, error) {
task, err := s.taskRepo.FindByID(taskID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -445,14 +482,16 @@ func (s *TaskService) UnarchiveTask(taskID, userID uint) (*responses.TaskRespons
return nil, err
}
resp := responses.NewTaskResponse(task)
return &resp, nil
return &responses.TaskWithSummaryResponse{
Data: responses.NewTaskResponse(task),
Summary: s.getSummaryForUser(userID),
}, nil
}
// === Task Completions ===
// CreateCompletion creates a task completion
func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest, userID uint) (*responses.TaskCompletionResponse, error) {
func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest, userID uint) (*responses.TaskCompletionWithSummaryResponse, error) {
// Get the task
task, err := s.taskRepo.FindByID(req.TaskID)
if err != nil {
@@ -537,7 +576,10 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest
// Non-fatal - still return the completion, just without the task
log.Warn().Err(err).Uint("task_id", req.TaskID).Msg("Failed to reload task after completion")
resp := responses.NewTaskCompletionResponse(completion)
return &resp, nil
return &responses.TaskCompletionWithSummaryResponse{
Data: resp,
Summary: s.getSummaryForUser(userID),
}, nil
}
// Send notification to residence owner and other users
@@ -545,7 +587,10 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest
// Return completion with updated task (includes kanban_column for UI update)
resp := responses.NewTaskCompletionWithTaskResponse(completion, task, 30)
return &resp, nil
return &responses.TaskCompletionWithSummaryResponse{
Data: resp,
Summary: s.getSummaryForUser(userID),
}, nil
}
// sendTaskCompletedNotification sends notifications when a task is completed
@@ -661,25 +706,32 @@ func (s *TaskService) ListCompletions(userID uint) ([]responses.TaskCompletionRe
}
// DeleteCompletion deletes a task completion
func (s *TaskService) DeleteCompletion(completionID, userID uint) error {
func (s *TaskService) DeleteCompletion(completionID, userID uint) (*responses.DeleteWithSummaryResponse, error) {
completion, err := s.taskRepo.FindCompletionByID(completionID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrCompletionNotFound
return nil, ErrCompletionNotFound
}
return err
return nil, err
}
// Check access
hasAccess, err := s.residenceRepo.HasAccess(completion.Task.ResidenceID, userID)
if err != nil {
return err
return nil, err
}
if !hasAccess {
return ErrTaskAccessDenied
return nil, ErrTaskAccessDenied
}
return s.taskRepo.DeleteCompletion(completionID)
if err := s.taskRepo.DeleteCompletion(completionID); err != nil {
return nil, err
}
return &responses.DeleteWithSummaryResponse{
Data: "completion deleted",
Summary: s.getSummaryForUser(userID),
}, nil
}
// GetCompletionsByTask gets all completions for a specific task