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:
@@ -165,8 +165,17 @@ func (s *ResidenceService) GetSummary(userID uint) (*responses.TotalSummary, err
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
// CreateResidence creates a new residence
|
||||
func (s *ResidenceService) CreateResidence(req *requests.CreateResidenceRequest, ownerID uint) (*responses.ResidenceResponse, error) {
|
||||
// getSummaryForUser is a helper that returns summary for a user, or empty summary on error
|
||||
func (s *ResidenceService) getSummaryForUser(userID uint) responses.TotalSummary {
|
||||
summary, err := s.GetSummary(userID)
|
||||
if err != nil || summary == nil {
|
||||
return responses.TotalSummary{}
|
||||
}
|
||||
return *summary
|
||||
}
|
||||
|
||||
// CreateResidence creates a new residence and returns it with updated summary
|
||||
func (s *ResidenceService) CreateResidence(req *requests.CreateResidenceRequest, ownerID uint) (*responses.ResidenceWithSummaryResponse, error) {
|
||||
// TODO: Check subscription tier limits
|
||||
// count, err := s.residenceRepo.CountByOwner(ownerID)
|
||||
// if err != nil {
|
||||
@@ -217,12 +226,17 @@ func (s *ResidenceService) CreateResidence(req *requests.CreateResidenceRequest,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := responses.NewResidenceResponse(residence)
|
||||
return &resp, nil
|
||||
// Get updated summary
|
||||
summary := s.getSummaryForUser(ownerID)
|
||||
|
||||
return &responses.ResidenceWithSummaryResponse{
|
||||
Data: responses.NewResidenceResponse(residence),
|
||||
Summary: summary,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateResidence updates a residence
|
||||
func (s *ResidenceService) UpdateResidence(residenceID, userID uint, req *requests.UpdateResidenceRequest) (*responses.ResidenceResponse, error) {
|
||||
// UpdateResidence updates a residence and returns it with updated summary
|
||||
func (s *ResidenceService) UpdateResidence(residenceID, userID uint, req *requests.UpdateResidenceRequest) (*responses.ResidenceWithSummaryResponse, error) {
|
||||
// Check ownership
|
||||
isOwner, err := s.residenceRepo.IsOwner(residenceID, userID)
|
||||
if err != nil {
|
||||
@@ -303,22 +317,37 @@ func (s *ResidenceService) UpdateResidence(residenceID, userID uint, req *reques
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := responses.NewResidenceResponse(residence)
|
||||
return &resp, nil
|
||||
// Get updated summary
|
||||
summary := s.getSummaryForUser(userID)
|
||||
|
||||
return &responses.ResidenceWithSummaryResponse{
|
||||
Data: responses.NewResidenceResponse(residence),
|
||||
Summary: summary,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteResidence soft-deletes a residence
|
||||
func (s *ResidenceService) DeleteResidence(residenceID, userID uint) error {
|
||||
// DeleteResidence soft-deletes a residence and returns updated summary
|
||||
func (s *ResidenceService) DeleteResidence(residenceID, userID uint) (*responses.ResidenceDeleteWithSummaryResponse, error) {
|
||||
// Check ownership
|
||||
isOwner, err := s.residenceRepo.IsOwner(residenceID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if !isOwner {
|
||||
return ErrNotResidenceOwner
|
||||
return nil, ErrNotResidenceOwner
|
||||
}
|
||||
|
||||
return s.residenceRepo.Delete(residenceID)
|
||||
if err := s.residenceRepo.Delete(residenceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get updated summary
|
||||
summary := s.getSummaryForUser(userID)
|
||||
|
||||
return &responses.ResidenceDeleteWithSummaryResponse{
|
||||
Data: "residence deleted",
|
||||
Summary: summary,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GenerateShareCode generates a new share code for a residence
|
||||
@@ -427,9 +456,13 @@ func (s *ResidenceService) JoinWithCode(code string, userID uint) (*responses.Jo
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get updated summary for the user
|
||||
summary := s.getSummaryForUser(userID)
|
||||
|
||||
return &responses.JoinResidenceResponse{
|
||||
Message: "Successfully joined residence",
|
||||
Residence: responses.NewResidenceResponse(residence),
|
||||
Summary: summary,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user