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:
@@ -69,6 +69,18 @@ type MyResidencesResponse struct {
|
|||||||
Summary TotalSummary `json:"summary"`
|
Summary TotalSummary `json:"summary"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResidenceWithSummaryResponse wraps ResidenceResponse with TotalSummary for CRUD operations
|
||||||
|
type ResidenceWithSummaryResponse struct {
|
||||||
|
Data ResidenceResponse `json:"data"`
|
||||||
|
Summary TotalSummary `json:"summary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResidenceDeleteWithSummaryResponse for delete operations
|
||||||
|
type ResidenceDeleteWithSummaryResponse struct {
|
||||||
|
Data string `json:"data"`
|
||||||
|
Summary TotalSummary `json:"summary"`
|
||||||
|
}
|
||||||
|
|
||||||
// ShareCodeResponse represents a share code in the API response
|
// ShareCodeResponse represents a share code in the API response
|
||||||
type ShareCodeResponse struct {
|
type ShareCodeResponse struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
@@ -84,6 +96,7 @@ type ShareCodeResponse struct {
|
|||||||
type JoinResidenceResponse struct {
|
type JoinResidenceResponse struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Residence ResidenceResponse `json:"residence"`
|
Residence ResidenceResponse `json:"residence"`
|
||||||
|
Summary TotalSummary `json:"summary"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateShareCodeResponse represents the response after generating a share code
|
// GenerateShareCodeResponse represents the response after generating a share code
|
||||||
|
|||||||
@@ -371,3 +371,24 @@ func NewTaskCompletionWithTaskResponse(c *models.TaskCompletion, task *models.Ta
|
|||||||
func DetermineKanbanColumn(task *models.Task, daysThreshold int) string {
|
func DetermineKanbanColumn(task *models.Task, daysThreshold int) string {
|
||||||
return categorization.DetermineKanbanColumn(task, daysThreshold)
|
return categorization.DetermineKanbanColumn(task, daysThreshold)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Response Wrappers with Summary ===
|
||||||
|
// These wrap CRUD responses with TotalSummary to eliminate extra API calls
|
||||||
|
|
||||||
|
// TaskWithSummaryResponse wraps TaskResponse with TotalSummary
|
||||||
|
type TaskWithSummaryResponse struct {
|
||||||
|
Data TaskResponse `json:"data"`
|
||||||
|
Summary TotalSummary `json:"summary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskCompletionWithSummaryResponse wraps TaskCompletionResponse with TotalSummary
|
||||||
|
type TaskCompletionWithSummaryResponse struct {
|
||||||
|
Data TaskCompletionResponse `json:"data"`
|
||||||
|
Summary TotalSummary `json:"summary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteWithSummaryResponse for delete operations
|
||||||
|
type DeleteWithSummaryResponse struct {
|
||||||
|
Data string `json:"data"`
|
||||||
|
Summary TotalSummary `json:"summary"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ func (h *ResidenceHandler) DeleteResidence(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.residenceService.DeleteResidence(uint(residenceID), user.ID)
|
response, err := h.residenceService.DeleteResidence(uint(residenceID), user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrResidenceNotFound):
|
case errors.Is(err, services.ErrResidenceNotFound):
|
||||||
@@ -174,7 +174,7 @@ func (h *ResidenceHandler) DeleteResidence(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.residence_deleted")})
|
c.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateShareCode handles POST /api/residences/:id/generate-share-code/
|
// GenerateShareCode handles POST /api/residences/:id/generate-share-code/
|
||||||
|
|||||||
@@ -55,13 +55,18 @@ func TestResidenceHandler_CreateResidence(t *testing.T) {
|
|||||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "My House", response["name"])
|
// Response should be wrapped in WithSummaryResponse
|
||||||
assert.Equal(t, "123 Main St", response["street_address"])
|
assert.Contains(t, response, "data")
|
||||||
assert.Equal(t, "Austin", response["city"])
|
assert.Contains(t, response, "summary")
|
||||||
assert.Equal(t, "TX", response["state_province"])
|
|
||||||
assert.Equal(t, "78701", response["postal_code"])
|
residenceData := response["data"].(map[string]interface{})
|
||||||
assert.Equal(t, "USA", response["country"]) // Default
|
assert.Equal(t, "My House", residenceData["name"])
|
||||||
assert.Equal(t, true, response["is_primary"])
|
assert.Equal(t, "123 Main St", residenceData["street_address"])
|
||||||
|
assert.Equal(t, "Austin", residenceData["city"])
|
||||||
|
assert.Equal(t, "TX", residenceData["state_province"])
|
||||||
|
assert.Equal(t, "78701", residenceData["postal_code"])
|
||||||
|
assert.Equal(t, "USA", residenceData["country"]) // Default
|
||||||
|
assert.Equal(t, true, residenceData["is_primary"])
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("creation with optional fields", func(t *testing.T) {
|
t.Run("creation with optional fields", func(t *testing.T) {
|
||||||
@@ -91,11 +96,16 @@ func TestResidenceHandler_CreateResidence(t *testing.T) {
|
|||||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, float64(3), response["bedrooms"])
|
// Response should be wrapped in WithSummaryResponse
|
||||||
assert.Equal(t, "2.5", response["bathrooms"]) // Decimal serializes as string
|
assert.Contains(t, response, "data")
|
||||||
assert.Equal(t, float64(2000), response["square_footage"])
|
assert.Contains(t, response, "summary")
|
||||||
|
|
||||||
|
residenceData := response["data"].(map[string]interface{})
|
||||||
|
assert.Equal(t, float64(3), residenceData["bedrooms"])
|
||||||
|
assert.Equal(t, "2.5", residenceData["bathrooms"]) // Decimal serializes as string
|
||||||
|
assert.Equal(t, float64(2000), residenceData["square_footage"])
|
||||||
// Note: first residence becomes primary by default even if is_primary=false is specified
|
// Note: first residence becomes primary by default even if is_primary=false is specified
|
||||||
assert.Contains(t, []interface{}{true, false}, response["is_primary"])
|
assert.Contains(t, []interface{}{true, false}, residenceData["is_primary"])
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("creation with missing required fields", func(t *testing.T) {
|
t.Run("creation with missing required fields", func(t *testing.T) {
|
||||||
@@ -214,8 +224,13 @@ func TestResidenceHandler_UpdateResidence(t *testing.T) {
|
|||||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "Updated Name", response["name"])
|
// Response should be wrapped in WithSummaryResponse
|
||||||
assert.Equal(t, "Dallas", response["city"])
|
assert.Contains(t, response, "data")
|
||||||
|
assert.Contains(t, response, "summary")
|
||||||
|
|
||||||
|
residenceData := response["data"].(map[string]interface{})
|
||||||
|
assert.Equal(t, "Updated Name", residenceData["name"])
|
||||||
|
assert.Equal(t, "Dallas", residenceData["city"])
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("shared user cannot update", func(t *testing.T) {
|
t.Run("shared user cannot update", func(t *testing.T) {
|
||||||
@@ -258,8 +273,14 @@ func TestResidenceHandler_DeleteResidence(t *testing.T) {
|
|||||||
|
|
||||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||||
|
|
||||||
response := testutil.ParseJSON(t, w.Body.Bytes())
|
var response map[string]interface{}
|
||||||
assert.Contains(t, response["message"], "deleted")
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Response should be wrapped in WithSummaryResponse
|
||||||
|
assert.Contains(t, response, "data")
|
||||||
|
assert.Contains(t, response, "summary")
|
||||||
|
assert.Contains(t, response["data"], "deleted")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,6 +347,10 @@ func TestResidenceHandler_JoinWithCode(t *testing.T) {
|
|||||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// JoinResidenceResponse includes summary
|
||||||
|
assert.Contains(t, response, "residence")
|
||||||
|
assert.Contains(t, response, "summary")
|
||||||
|
|
||||||
residenceResp := response["residence"].(map[string]interface{})
|
residenceResp := response["residence"].(map[string]interface{})
|
||||||
assert.Equal(t, "Join Test", residenceResp["name"])
|
assert.Equal(t, "Join Test", residenceResp["name"])
|
||||||
})
|
})
|
||||||
@@ -457,23 +482,34 @@ func TestResidenceHandler_JSONResponses(t *testing.T) {
|
|||||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Required fields
|
// Response should be wrapped in WithSummaryResponse
|
||||||
assert.Contains(t, response, "id")
|
assert.Contains(t, response, "data")
|
||||||
assert.Contains(t, response, "name")
|
assert.Contains(t, response, "summary")
|
||||||
assert.Contains(t, response, "street_address")
|
|
||||||
assert.Contains(t, response, "city")
|
residenceData := response["data"].(map[string]interface{})
|
||||||
assert.Contains(t, response, "state_province")
|
|
||||||
assert.Contains(t, response, "postal_code")
|
// Required fields in residence data
|
||||||
assert.Contains(t, response, "country")
|
assert.Contains(t, residenceData, "id")
|
||||||
assert.Contains(t, response, "is_primary")
|
assert.Contains(t, residenceData, "name")
|
||||||
assert.Contains(t, response, "is_active")
|
assert.Contains(t, residenceData, "street_address")
|
||||||
assert.Contains(t, response, "created_at")
|
assert.Contains(t, residenceData, "city")
|
||||||
assert.Contains(t, response, "updated_at")
|
assert.Contains(t, residenceData, "state_province")
|
||||||
|
assert.Contains(t, residenceData, "postal_code")
|
||||||
|
assert.Contains(t, residenceData, "country")
|
||||||
|
assert.Contains(t, residenceData, "is_primary")
|
||||||
|
assert.Contains(t, residenceData, "is_active")
|
||||||
|
assert.Contains(t, residenceData, "created_at")
|
||||||
|
assert.Contains(t, residenceData, "updated_at")
|
||||||
|
|
||||||
// Type checks
|
// Type checks
|
||||||
assert.IsType(t, float64(0), response["id"])
|
assert.IsType(t, float64(0), residenceData["id"])
|
||||||
assert.IsType(t, "", response["name"])
|
assert.IsType(t, "", residenceData["name"])
|
||||||
assert.IsType(t, true, response["is_primary"])
|
assert.IsType(t, true, residenceData["is_primary"])
|
||||||
|
|
||||||
|
// Summary should have expected fields
|
||||||
|
summary := response["summary"].(map[string]interface{})
|
||||||
|
assert.Contains(t, summary, "total_residences")
|
||||||
|
assert.Contains(t, summary, "total_tasks")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("list response returns array", func(t *testing.T) {
|
t.Run("list response returns array", func(t *testing.T) {
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ func (h *TaskHandler) DeleteTask(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.taskService.DeleteTask(uint(taskID), user.ID)
|
response, err := h.taskService.DeleteTask(uint(taskID), user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrTaskNotFound):
|
case errors.Is(err, services.ErrTaskNotFound):
|
||||||
@@ -168,7 +168,7 @@ func (h *TaskHandler) DeleteTask(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.task_deleted")})
|
c.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkInProgress handles POST /api/tasks/:id/mark-in-progress/
|
// MarkInProgress handles POST /api/tasks/:id/mark-in-progress/
|
||||||
@@ -192,7 +192,7 @@ func (h *TaskHandler) MarkInProgress(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.task_in_progress"), "task": response})
|
c.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CancelTask handles POST /api/tasks/:id/cancel/
|
// CancelTask handles POST /api/tasks/:id/cancel/
|
||||||
@@ -218,7 +218,7 @@ func (h *TaskHandler) CancelTask(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.task_cancelled"), "task": response})
|
c.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UncancelTask handles POST /api/tasks/:id/uncancel/
|
// UncancelTask handles POST /api/tasks/:id/uncancel/
|
||||||
@@ -242,7 +242,7 @@ func (h *TaskHandler) UncancelTask(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.task_uncancelled"), "task": response})
|
c.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ArchiveTask handles POST /api/tasks/:id/archive/
|
// ArchiveTask handles POST /api/tasks/:id/archive/
|
||||||
@@ -268,7 +268,7 @@ func (h *TaskHandler) ArchiveTask(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.task_archived"), "task": response})
|
c.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnarchiveTask handles POST /api/tasks/:id/unarchive/
|
// UnarchiveTask handles POST /api/tasks/:id/unarchive/
|
||||||
@@ -292,7 +292,7 @@ func (h *TaskHandler) UnarchiveTask(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.task_unarchived"), "task": response})
|
c.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Task Completions ===
|
// === Task Completions ===
|
||||||
@@ -455,7 +455,7 @@ func (h *TaskHandler) DeleteCompletion(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.taskService.DeleteCompletion(uint(completionID), user.ID)
|
response, err := h.taskService.DeleteCompletion(uint(completionID), user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, services.ErrCompletionNotFound):
|
case errors.Is(err, services.ErrCompletionNotFound):
|
||||||
@@ -467,7 +467,7 @@ func (h *TaskHandler) DeleteCompletion(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.completion_deleted")})
|
c.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Lookups ===
|
// === Lookups ===
|
||||||
|
|||||||
@@ -55,11 +55,16 @@ func TestTaskHandler_CreateTask(t *testing.T) {
|
|||||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "Fix leaky faucet", response["title"])
|
// Response should be wrapped in WithSummaryResponse
|
||||||
assert.Equal(t, "Kitchen faucet is dripping", response["description"])
|
assert.Contains(t, response, "data")
|
||||||
assert.Equal(t, float64(residence.ID), response["residence_id"])
|
assert.Contains(t, response, "summary")
|
||||||
assert.Equal(t, false, response["is_cancelled"])
|
|
||||||
assert.Equal(t, false, response["is_archived"])
|
taskData := response["data"].(map[string]interface{})
|
||||||
|
assert.Equal(t, "Fix leaky faucet", taskData["title"])
|
||||||
|
assert.Equal(t, "Kitchen faucet is dripping", taskData["description"])
|
||||||
|
assert.Equal(t, float64(residence.ID), taskData["residence_id"])
|
||||||
|
assert.Equal(t, false, taskData["is_cancelled"])
|
||||||
|
assert.Equal(t, false, taskData["is_archived"])
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("task creation with optional fields", func(t *testing.T) {
|
t.Run("task creation with optional fields", func(t *testing.T) {
|
||||||
@@ -89,10 +94,15 @@ func TestTaskHandler_CreateTask(t *testing.T) {
|
|||||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "Install new lights", response["title"])
|
// Response should be wrapped in WithSummaryResponse
|
||||||
assert.NotNil(t, response["category"])
|
assert.Contains(t, response, "data")
|
||||||
assert.NotNil(t, response["priority"])
|
assert.Contains(t, response, "summary")
|
||||||
assert.Equal(t, "150.5", response["estimated_cost"]) // Decimal serializes as string
|
|
||||||
|
taskData := response["data"].(map[string]interface{})
|
||||||
|
assert.Equal(t, "Install new lights", taskData["title"])
|
||||||
|
assert.NotNil(t, taskData["category"])
|
||||||
|
assert.NotNil(t, taskData["priority"])
|
||||||
|
assert.Equal(t, "150.5", taskData["estimated_cost"]) // Decimal serializes as string
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("task creation without residence access", func(t *testing.T) {
|
t.Run("task creation without residence access", func(t *testing.T) {
|
||||||
@@ -267,8 +277,13 @@ func TestTaskHandler_UpdateTask(t *testing.T) {
|
|||||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "Updated Title", response["title"])
|
// Response should be wrapped in WithSummaryResponse
|
||||||
assert.Equal(t, "Updated description", response["description"])
|
assert.Contains(t, response, "data")
|
||||||
|
assert.Contains(t, response, "summary")
|
||||||
|
|
||||||
|
taskData := response["data"].(map[string]interface{})
|
||||||
|
assert.Equal(t, "Updated Title", taskData["title"])
|
||||||
|
assert.Equal(t, "Updated description", taskData["description"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,8 +302,14 @@ func TestTaskHandler_DeleteTask(t *testing.T) {
|
|||||||
|
|
||||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||||
|
|
||||||
response := testutil.ParseJSON(t, w.Body.Bytes())
|
var response map[string]interface{}
|
||||||
assert.Contains(t, response["message"], "deleted")
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Response should be wrapped in WithSummaryResponse
|
||||||
|
assert.Contains(t, response, "data")
|
||||||
|
assert.Contains(t, response, "summary")
|
||||||
|
assert.Contains(t, response["data"], "deleted")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,10 +332,12 @@ func TestTaskHandler_CancelTask(t *testing.T) {
|
|||||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Contains(t, response["message"], "cancelled")
|
// Response should be wrapped in WithSummaryResponse
|
||||||
|
assert.Contains(t, response, "data")
|
||||||
|
assert.Contains(t, response, "summary")
|
||||||
|
|
||||||
taskResp := response["task"].(map[string]interface{})
|
taskData := response["data"].(map[string]interface{})
|
||||||
assert.Equal(t, true, taskResp["is_cancelled"])
|
assert.Equal(t, true, taskData["is_cancelled"])
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("cancel already cancelled task", func(t *testing.T) {
|
t.Run("cancel already cancelled task", func(t *testing.T) {
|
||||||
@@ -348,8 +371,12 @@ func TestTaskHandler_UncancelTask(t *testing.T) {
|
|||||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
taskResp := response["task"].(map[string]interface{})
|
// Response should be wrapped in WithSummaryResponse
|
||||||
assert.Equal(t, false, taskResp["is_cancelled"])
|
assert.Contains(t, response, "data")
|
||||||
|
assert.Contains(t, response, "summary")
|
||||||
|
|
||||||
|
taskData := response["data"].(map[string]interface{})
|
||||||
|
assert.Equal(t, false, taskData["is_cancelled"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,8 +399,12 @@ func TestTaskHandler_ArchiveTask(t *testing.T) {
|
|||||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
taskResp := response["task"].(map[string]interface{})
|
// Response should be wrapped in WithSummaryResponse
|
||||||
assert.Equal(t, true, taskResp["is_archived"])
|
assert.Contains(t, response, "data")
|
||||||
|
assert.Contains(t, response, "summary")
|
||||||
|
|
||||||
|
taskData := response["data"].(map[string]interface{})
|
||||||
|
assert.Equal(t, true, taskData["is_archived"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,8 +431,12 @@ func TestTaskHandler_UnarchiveTask(t *testing.T) {
|
|||||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
taskResp := response["task"].(map[string]interface{})
|
// Response should be wrapped in WithSummaryResponse
|
||||||
assert.Equal(t, false, taskResp["is_archived"])
|
assert.Contains(t, response, "data")
|
||||||
|
assert.Contains(t, response, "summary")
|
||||||
|
|
||||||
|
taskData := response["data"].(map[string]interface{})
|
||||||
|
assert.Equal(t, false, taskData["is_archived"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,8 +460,10 @@ func TestTaskHandler_MarkInProgress(t *testing.T) {
|
|||||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Contains(t, response["message"], "in progress")
|
// Response should be wrapped in WithSummaryResponse
|
||||||
assert.NotNil(t, response["task"])
|
assert.Contains(t, response, "data")
|
||||||
|
assert.Contains(t, response, "summary")
|
||||||
|
assert.NotNil(t, response["data"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,9 +493,14 @@ func TestTaskHandler_CreateCompletion(t *testing.T) {
|
|||||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
testutil.AssertJSONFieldExists(t, response, "id")
|
// Response should be wrapped in WithSummaryResponse
|
||||||
assert.Equal(t, float64(task.ID), response["task_id"])
|
assert.Contains(t, response, "data")
|
||||||
assert.Equal(t, "Completed successfully", response["notes"])
|
assert.Contains(t, response, "summary")
|
||||||
|
|
||||||
|
completionData := response["data"].(map[string]interface{})
|
||||||
|
testutil.AssertJSONFieldExists(t, completionData, "id")
|
||||||
|
assert.Equal(t, float64(task.ID), completionData["task_id"])
|
||||||
|
assert.Equal(t, "Completed successfully", completionData["notes"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,8 +590,14 @@ func TestTaskHandler_DeleteCompletion(t *testing.T) {
|
|||||||
|
|
||||||
testutil.AssertStatusCode(t, w, http.StatusOK)
|
testutil.AssertStatusCode(t, w, http.StatusOK)
|
||||||
|
|
||||||
response := testutil.ParseJSON(t, w.Body.Bytes())
|
var response map[string]interface{}
|
||||||
assert.Contains(t, response["message"], "deleted")
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Response should be wrapped in WithSummaryResponse
|
||||||
|
assert.Contains(t, response, "data")
|
||||||
|
assert.Contains(t, response, "summary")
|
||||||
|
assert.Contains(t, response["data"], "deleted")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -645,22 +693,35 @@ func TestTaskHandler_JSONResponses(t *testing.T) {
|
|||||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Required fields
|
// Response should be wrapped in WithSummaryResponse
|
||||||
assert.Contains(t, response, "id")
|
assert.Contains(t, response, "data")
|
||||||
assert.Contains(t, response, "residence_id")
|
assert.Contains(t, response, "summary")
|
||||||
assert.Contains(t, response, "created_by_id")
|
|
||||||
assert.Contains(t, response, "title")
|
taskData := response["data"].(map[string]interface{})
|
||||||
assert.Contains(t, response, "description")
|
|
||||||
assert.Contains(t, response, "is_cancelled")
|
// Required fields in task data
|
||||||
assert.Contains(t, response, "is_archived")
|
assert.Contains(t, taskData, "id")
|
||||||
assert.Contains(t, response, "created_at")
|
assert.Contains(t, taskData, "residence_id")
|
||||||
assert.Contains(t, response, "updated_at")
|
assert.Contains(t, taskData, "created_by_id")
|
||||||
|
assert.Contains(t, taskData, "title")
|
||||||
|
assert.Contains(t, taskData, "description")
|
||||||
|
assert.Contains(t, taskData, "is_cancelled")
|
||||||
|
assert.Contains(t, taskData, "is_archived")
|
||||||
|
assert.Contains(t, taskData, "created_at")
|
||||||
|
assert.Contains(t, taskData, "updated_at")
|
||||||
|
|
||||||
// Type checks
|
// Type checks
|
||||||
assert.IsType(t, float64(0), response["id"])
|
assert.IsType(t, float64(0), taskData["id"])
|
||||||
assert.IsType(t, "", response["title"])
|
assert.IsType(t, "", taskData["title"])
|
||||||
assert.IsType(t, false, response["is_cancelled"])
|
assert.IsType(t, false, taskData["is_cancelled"])
|
||||||
assert.IsType(t, false, response["is_archived"])
|
assert.IsType(t, false, taskData["is_archived"])
|
||||||
|
|
||||||
|
// Summary should have expected fields
|
||||||
|
summary := response["summary"].(map[string]interface{})
|
||||||
|
assert.Contains(t, summary, "total_residences")
|
||||||
|
assert.Contains(t, summary, "total_tasks")
|
||||||
|
assert.Contains(t, summary, "total_pending")
|
||||||
|
assert.Contains(t, summary, "total_overdue")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("list response returns kanban board", func(t *testing.T) {
|
t.Run("list response returns kanban board", func(t *testing.T) {
|
||||||
|
|||||||
@@ -96,9 +96,10 @@ func SetupRouter(deps *Dependencies) *gin.Engine {
|
|||||||
documentService := services.NewDocumentService(documentRepo, residenceRepo)
|
documentService := services.NewDocumentService(documentRepo, residenceRepo)
|
||||||
notificationService := services.NewNotificationService(notificationRepo, deps.PushClient)
|
notificationService := services.NewNotificationService(notificationRepo, deps.PushClient)
|
||||||
|
|
||||||
// Wire up notification and email services to task service (for task completion notifications)
|
// Wire up notification, email, and residence services to task service
|
||||||
taskService.SetNotificationService(notificationService)
|
taskService.SetNotificationService(notificationService)
|
||||||
taskService.SetEmailService(deps.EmailService)
|
taskService.SetEmailService(deps.EmailService)
|
||||||
|
taskService.SetResidenceService(residenceService) // For including TotalSummary in CRUD responses
|
||||||
subscriptionService := services.NewSubscriptionService(subscriptionRepo, residenceRepo, taskRepo, contractorRepo, documentRepo)
|
subscriptionService := services.NewSubscriptionService(subscriptionRepo, residenceRepo, taskRepo, contractorRepo, documentRepo)
|
||||||
taskTemplateService := services.NewTaskTemplateService(taskTemplateRepo)
|
taskTemplateService := services.NewTaskTemplateService(taskTemplateRepo)
|
||||||
|
|
||||||
|
|||||||
@@ -165,8 +165,17 @@ func (s *ResidenceService) GetSummary(userID uint) (*responses.TotalSummary, err
|
|||||||
return summary, nil
|
return summary, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateResidence creates a new residence
|
// getSummaryForUser is a helper that returns summary for a user, or empty summary on error
|
||||||
func (s *ResidenceService) CreateResidence(req *requests.CreateResidenceRequest, ownerID uint) (*responses.ResidenceResponse, 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
|
// TODO: Check subscription tier limits
|
||||||
// count, err := s.residenceRepo.CountByOwner(ownerID)
|
// count, err := s.residenceRepo.CountByOwner(ownerID)
|
||||||
// if err != nil {
|
// if err != nil {
|
||||||
@@ -217,12 +226,17 @@ func (s *ResidenceService) CreateResidence(req *requests.CreateResidenceRequest,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := responses.NewResidenceResponse(residence)
|
// Get updated summary
|
||||||
return &resp, nil
|
summary := s.getSummaryForUser(ownerID)
|
||||||
|
|
||||||
|
return &responses.ResidenceWithSummaryResponse{
|
||||||
|
Data: responses.NewResidenceResponse(residence),
|
||||||
|
Summary: summary,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateResidence updates a residence
|
// UpdateResidence updates a residence and returns it with updated summary
|
||||||
func (s *ResidenceService) UpdateResidence(residenceID, userID uint, req *requests.UpdateResidenceRequest) (*responses.ResidenceResponse, error) {
|
func (s *ResidenceService) UpdateResidence(residenceID, userID uint, req *requests.UpdateResidenceRequest) (*responses.ResidenceWithSummaryResponse, error) {
|
||||||
// Check ownership
|
// Check ownership
|
||||||
isOwner, err := s.residenceRepo.IsOwner(residenceID, userID)
|
isOwner, err := s.residenceRepo.IsOwner(residenceID, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -303,22 +317,37 @@ func (s *ResidenceService) UpdateResidence(residenceID, userID uint, req *reques
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := responses.NewResidenceResponse(residence)
|
// Get updated summary
|
||||||
return &resp, nil
|
summary := s.getSummaryForUser(userID)
|
||||||
|
|
||||||
|
return &responses.ResidenceWithSummaryResponse{
|
||||||
|
Data: responses.NewResidenceResponse(residence),
|
||||||
|
Summary: summary,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteResidence soft-deletes a residence
|
// DeleteResidence soft-deletes a residence and returns updated summary
|
||||||
func (s *ResidenceService) DeleteResidence(residenceID, userID uint) error {
|
func (s *ResidenceService) DeleteResidence(residenceID, userID uint) (*responses.ResidenceDeleteWithSummaryResponse, error) {
|
||||||
// Check ownership
|
// Check ownership
|
||||||
isOwner, err := s.residenceRepo.IsOwner(residenceID, userID)
|
isOwner, err := s.residenceRepo.IsOwner(residenceID, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !isOwner {
|
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
|
// 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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get updated summary for the user
|
||||||
|
summary := s.getSummaryForUser(userID)
|
||||||
|
|
||||||
return &responses.JoinResidenceResponse{
|
return &responses.JoinResidenceResponse{
|
||||||
Message: "Successfully joined residence",
|
Message: "Successfully joined residence",
|
||||||
Residence: responses.NewResidenceResponse(residence),
|
Residence: responses.NewResidenceResponse(residence),
|
||||||
|
Summary: summary,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ var (
|
|||||||
type TaskService struct {
|
type TaskService struct {
|
||||||
taskRepo *repositories.TaskRepository
|
taskRepo *repositories.TaskRepository
|
||||||
residenceRepo *repositories.ResidenceRepository
|
residenceRepo *repositories.ResidenceRepository
|
||||||
|
residenceService *ResidenceService
|
||||||
notificationService *NotificationService
|
notificationService *NotificationService
|
||||||
emailService *EmailService
|
emailService *EmailService
|
||||||
}
|
}
|
||||||
@@ -49,6 +50,23 @@ func (s *TaskService) SetEmailService(es *EmailService) {
|
|||||||
s.emailService = es
|
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 ===
|
// === Task CRUD ===
|
||||||
|
|
||||||
// GetTask gets a task by ID with access check
|
// 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
|
// 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
|
// Check residence access
|
||||||
hasAccess, err := s.residenceRepo.HasAccess(req.ResidenceID, userID)
|
hasAccess, err := s.residenceRepo.HasAccess(req.ResidenceID, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -168,12 +186,14 @@ func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint) (
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := responses.NewTaskResponse(task)
|
return &responses.TaskWithSummaryResponse{
|
||||||
return &resp, nil
|
Data: responses.NewTaskResponse(task),
|
||||||
|
Summary: s.getSummaryForUser(userID),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateTask updates a task
|
// 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)
|
task, err := s.taskRepo.FindByID(taskID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -242,36 +262,45 @@ func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRe
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := responses.NewTaskResponse(task)
|
return &responses.TaskWithSummaryResponse{
|
||||||
return &resp, nil
|
Data: responses.NewTaskResponse(task),
|
||||||
|
Summary: s.getSummaryForUser(userID),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteTask deletes a task
|
// 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)
|
task, err := s.taskRepo.FindByID(taskID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return ErrTaskNotFound
|
return nil, ErrTaskNotFound
|
||||||
}
|
}
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check access
|
// Check access
|
||||||
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
|
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !hasAccess {
|
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 ===
|
// === Task Actions ===
|
||||||
|
|
||||||
// MarkInProgress marks a task as in progress
|
// 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)
|
task, err := s.taskRepo.FindByID(taskID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -305,12 +334,14 @@ func (s *TaskService) MarkInProgress(taskID, userID uint) (*responses.TaskRespon
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := responses.NewTaskResponse(task)
|
return &responses.TaskWithSummaryResponse{
|
||||||
return &resp, nil
|
Data: responses.NewTaskResponse(task),
|
||||||
|
Summary: s.getSummaryForUser(userID),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CancelTask cancels a task
|
// 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)
|
task, err := s.taskRepo.FindByID(taskID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -342,12 +373,14 @@ func (s *TaskService) CancelTask(taskID, userID uint) (*responses.TaskResponse,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := responses.NewTaskResponse(task)
|
return &responses.TaskWithSummaryResponse{
|
||||||
return &resp, nil
|
Data: responses.NewTaskResponse(task),
|
||||||
|
Summary: s.getSummaryForUser(userID),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UncancelTask uncancels a task
|
// 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)
|
task, err := s.taskRepo.FindByID(taskID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -375,12 +408,14 @@ func (s *TaskService) UncancelTask(taskID, userID uint) (*responses.TaskResponse
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := responses.NewTaskResponse(task)
|
return &responses.TaskWithSummaryResponse{
|
||||||
return &resp, nil
|
Data: responses.NewTaskResponse(task),
|
||||||
|
Summary: s.getSummaryForUser(userID),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ArchiveTask archives a task
|
// 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)
|
task, err := s.taskRepo.FindByID(taskID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -412,12 +447,14 @@ func (s *TaskService) ArchiveTask(taskID, userID uint) (*responses.TaskResponse,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := responses.NewTaskResponse(task)
|
return &responses.TaskWithSummaryResponse{
|
||||||
return &resp, nil
|
Data: responses.NewTaskResponse(task),
|
||||||
|
Summary: s.getSummaryForUser(userID),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnarchiveTask unarchives a task
|
// 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)
|
task, err := s.taskRepo.FindByID(taskID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -445,14 +482,16 @@ func (s *TaskService) UnarchiveTask(taskID, userID uint) (*responses.TaskRespons
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := responses.NewTaskResponse(task)
|
return &responses.TaskWithSummaryResponse{
|
||||||
return &resp, nil
|
Data: responses.NewTaskResponse(task),
|
||||||
|
Summary: s.getSummaryForUser(userID),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Task Completions ===
|
// === Task Completions ===
|
||||||
|
|
||||||
// CreateCompletion creates a task completion
|
// 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
|
// Get the task
|
||||||
task, err := s.taskRepo.FindByID(req.TaskID)
|
task, err := s.taskRepo.FindByID(req.TaskID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -537,7 +576,10 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest
|
|||||||
// Non-fatal - still return the completion, just without the task
|
// 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")
|
log.Warn().Err(err).Uint("task_id", req.TaskID).Msg("Failed to reload task after completion")
|
||||||
resp := responses.NewTaskCompletionResponse(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
|
// 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)
|
// Return completion with updated task (includes kanban_column for UI update)
|
||||||
resp := responses.NewTaskCompletionWithTaskResponse(completion, task, 30)
|
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
|
// 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
|
// 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)
|
completion, err := s.taskRepo.FindCompletionByID(completionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return ErrCompletionNotFound
|
return nil, ErrCompletionNotFound
|
||||||
}
|
}
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check access
|
// Check access
|
||||||
hasAccess, err := s.residenceRepo.HasAccess(completion.Task.ResidenceID, userID)
|
hasAccess, err := s.residenceRepo.HasAccess(completion.Task.ResidenceID, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !hasAccess {
|
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
|
// GetCompletionsByTask gets all completions for a specific task
|
||||||
|
|||||||
Reference in New Issue
Block a user