diff --git a/internal/chore/api.go b/internal/chore/api.go index 5dfa720..7c1aa27 100644 --- a/internal/chore/api.go +++ b/internal/chore/api.go @@ -36,7 +36,7 @@ func (h *API) GetAllChores(c *gin.Context) { c.JSON(401, gin.H{"error": "Unauthorized"}) return } - chores, err := h.choreRepo.GetChores(c, user.CircleID, user.ID) + chores, err := h.choreRepo.GetChores(c, user.CircleID, user.ID, false) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return diff --git a/internal/chore/handler.go b/internal/chore/handler.go index 6e7fc3c..84ee124 100644 --- a/internal/chore/handler.go +++ b/internal/chore/handler.go @@ -53,6 +53,8 @@ type ChoreReq struct { Labels []string `json:"labels"` LabelsV2 *[]LabelReq `json:"labelsV2"` ThingTrigger *ThingTrigger `json:"thingTrigger"` + Points *int `json:"points"` + CompletionWindow *int `json:"completionWindow"` } type Handler struct { choreRepo *chRepo.ChoreRepository @@ -85,7 +87,13 @@ func (h *Handler) getChores(c *gin.Context) { }) return } - chores, err := h.choreRepo.GetChores(c, u.CircleID, u.ID) + includeArchived := false + + if c.Query("includeArchived") == "true" { + includeArchived = true + } + + chores, err := h.choreRepo.GetChores(c, u.CircleID, u.ID, includeArchived) if err != nil { c.JSON(500, gin.H{ "error": "Error getting chores", @@ -271,6 +279,8 @@ func (h *Handler) createChore(c *gin.Context) { CreatedBy: currentUser.ID, CreatedAt: time.Now().UTC(), CircleID: currentUser.CircleID, + Points: choreReq.Points, + CompletionWindow: choreReq.CompletionWindow, } id, err := h.choreRepo.CreateChore(c, createdChore) createdChore.ID = id @@ -530,6 +540,8 @@ func (h *Handler) editChore(c *gin.Context) { UpdatedBy: currentUser.ID, CreatedBy: oldChore.CreatedBy, CreatedAt: oldChore.CreatedAt, + Points: choreReq.Points, + CompletionWindow: choreReq.CompletionWindow, } if err := h.choreRepo.UpsertChore(c, updatedChore); err != nil { c.JSON(500, gin.H{ @@ -954,6 +966,16 @@ func (h *Handler) completeChore(c *gin.Context) { }) return } + // confirm that the chore in completion window: + if chore.CompletionWindow != nil { + if completedDate.After(chore.NextDueDate.Add(time.Hour * time.Duration(*chore.CompletionWindow))) { + c.JSON(400, gin.H{ + "error": "Chore is out of completion window", + }) + return + } + } + var nextDueDate *time.Time if chore.FrequencyType == "adaptive" { history, err := h.choreRepo.GetChoreHistoryWithLimit(c, chore.ID, 5) @@ -1197,6 +1219,45 @@ func (h *Handler) updatePriority(c *gin.Context) { }) } +func (h *Handler) getChoresHistory(c *gin.Context) { + + currentUser, ok := auth.CurrentUser(c) + if !ok { + c.JSON(500, gin.H{ + "error": "Error getting current user", + }) + return + } + durationRaw := c.Query("limit") + if durationRaw == "" { + durationRaw = "7" + } + + duration, err := strconv.Atoi(durationRaw) + if err != nil { + c.JSON(400, gin.H{ + "error": "Invalid duration", + }) + return + } + includeCircleRaw := c.Query("members") + includeCircle := false + if includeCircleRaw == "true" { + includeCircle = true + } + + choreHistories, err := h.choreRepo.GetChoresHistoryByUserID(c, currentUser.ID, currentUser.CircleID, duration, includeCircle) + if err != nil { + c.JSON(500, gin.H{ + "error": "Error getting chore history", + }) + return + } + c.JSON(200, gin.H{ + "res": choreHistories, + }) +} + func (h *Handler) DeleteHistory(c *gin.Context) { currentUser, ok := auth.CurrentUser(c) @@ -1361,7 +1422,7 @@ func Routes(router *gin.Engine, h *Handler, auth *jwt.GinJWTMiddleware) { { choresRoutes.GET("/", h.getChores) choresRoutes.GET("/archived", h.getArchivedChores) - + choresRoutes.GET("/history", h.getChoresHistory) choresRoutes.PUT("/", h.editChore) choresRoutes.PUT("/:id/priority", h.updatePriority) choresRoutes.POST("/", h.createChore) diff --git a/internal/chore/model/model.go b/internal/chore/model/model.go index 4f335c7..4f15219 100644 --- a/internal/chore/model/model.go +++ b/internal/chore/model/model.go @@ -46,6 +46,8 @@ type Chore struct { ThingChore *tModel.ThingChore `json:"thingChore" gorm:"foreignkey:chore_id;references:id;<-:false"` // ThingChore relationship Status int `json:"status" gorm:"column:status"` Priority int `json:"priority" gorm:"column:priority"` + CompletionWindow *int `json:"completionWindow,omitempty" gorm:"column:completion_window"` // Number seconds before the chore is due that it can be completed + Points *int `json:"points,omitempty" gorm:"column:points"` // Points for completing the chore } type ChoreAssignees struct { ID int `json:"-" gorm:"primary_key"` @@ -53,15 +55,26 @@ type ChoreAssignees struct { UserID int `json:"userId" gorm:"column:user_id;uniqueIndex:idx_chore_user"` // The user this assignee is for } type ChoreHistory struct { - ID int `json:"id" gorm:"primary_key"` // Unique identifier - ChoreID int `json:"choreId" gorm:"column:chore_id"` // The chore this history is for - CompletedAt *time.Time `json:"completedAt" gorm:"column:completed_at"` // When the chore was completed - CompletedBy int `json:"completedBy" gorm:"column:completed_by"` // Who completed the chore - AssignedTo int `json:"assignedTo" gorm:"column:assigned_to"` // Who the chore was assigned to - Note *string `json:"notes" gorm:"column:notes"` // Notes about the chore - DueDate *time.Time `json:"dueDate" gorm:"column:due_date"` // When the chore was due - UpdatedAt *time.Time `json:"updatedAt" gorm:"column:updated_at"` // When the record was last updated + ID int `json:"id" gorm:"primary_key"` // Unique identifier + ChoreID int `json:"choreId" gorm:"column:chore_id"` // The chore this history is for + CompletedAt *time.Time `json:"completedAt" gorm:"column:completed_at"` // When the chore was completed + CompletedBy int `json:"completedBy" gorm:"column:completed_by"` // Who completed the chore + AssignedTo int `json:"assignedTo" gorm:"column:assigned_to"` // Who the chore was assigned to + Note *string `json:"notes" gorm:"column:notes"` // Notes about the chore + DueDate *time.Time `json:"dueDate" gorm:"column:due_date"` // When the chore was due + UpdatedAt *time.Time `json:"updatedAt" gorm:"column:updated_at"` // When the record was last updated + Status ChoreHistoryStatus `json:"status" gorm:"column:status"` // Status of the chore + Points *int `json:"points,omitempty" gorm:"column:points"` // Points for completing the chore } +type ChoreHistoryStatus int8 + +const ( + ChoreHistoryStatusPending ChoreHistoryStatus = 0 + ChoreHistoryStatusCompleted ChoreHistoryStatus = 1 + ChoreHistoryStatusCompletedLate ChoreHistoryStatus = 2 + ChoreHistoryStatusMissed ChoreHistoryStatus = 3 + ChoreHistoryStatusSkipped ChoreHistoryStatus = 4 +) type FrequencyMetadata struct { Days []*string `json:"days,omitempty"` @@ -96,6 +109,7 @@ type ChoreDetail struct { Priority int `json:"priority" gorm:"column:priority"` Notes *string `json:"notes" gorm:"column:notes"` CreatedBy int `json:"createdBy" gorm:"column:created_by"` + CompletionWindow *int `json:"completionWindow,omitempty" gorm:"column:completion_window"` } type Label struct { diff --git a/internal/chore/repo/repository.go b/internal/chore/repo/repository.go index 0a43938..1643f84 100644 --- a/internal/chore/repo/repository.go +++ b/internal/chore/repo/repository.go @@ -8,6 +8,7 @@ import ( config "donetick.com/core/config" chModel "donetick.com/core/internal/chore/model" + cModel "donetick.com/core/internal/circle/model" "gorm.io/gorm" ) @@ -49,10 +50,13 @@ func (r *ChoreRepository) GetChore(c context.Context, choreID int) (*chModel.Cho return &chore, nil } -func (r *ChoreRepository) GetChores(c context.Context, circleID int, userID int) ([]*chModel.Chore, error) { +func (r *ChoreRepository) GetChores(c context.Context, circleID int, userID int, includeArchived bool) ([]*chModel.Chore, error) { var chores []*chModel.Chore - // if err := r.db.WithContext(c).Preload("Assignees").Where("is_active = ?", true).Order("next_due_date asc").Find(&chores, "circle_id = ?", circleID).Error; err != nil { - if err := r.db.WithContext(c).Preload("Assignees").Preload("LabelsV2").Joins("left join chore_assignees on chores.id = chore_assignees.chore_id").Where("chores.circle_id = ? AND (chores.created_by = ? OR chore_assignees.user_id = ?)", circleID, userID, userID).Group("chores.id").Order("next_due_date asc").Find(&chores, "circle_id = ? AND is_active = ?", circleID, true).Error; err != nil { + query := r.db.WithContext(c).Preload("Assignees").Preload("LabelsV2").Joins("left join chore_assignees on chores.id = chore_assignees.chore_id").Where("chores.circle_id = ? AND (chores.created_by = ? OR chore_assignees.user_id = ?)", circleID, userID, userID).Group("chores.id").Order("next_due_date asc") + if !includeArchived { + query = query.Where("chores.is_active = ?", true) + } + if err := query.Find(&chores, "circle_id = ?", circleID).Error; err != nil { return nil, err } return chores, nil @@ -98,6 +102,7 @@ func (r *ChoreRepository) CompleteChore(c context.Context, chore *chModel.Chore, AssignedTo: chore.AssignedTo, DueDate: chore.NextDueDate, Note: note, + Points: chore.Points, } if err := tx.Create(ch).Error; err != nil { return err @@ -114,6 +119,12 @@ func (r *ChoreRepository) CompleteChore(c context.Context, chore *chModel.Chore, if err := tx.Model(&chModel.Chore{}).Where("id = ?", chore.ID).Updates(updates).Error; err != nil { return err } + // Update UserCirclee Points : + if chore.Points != nil && *chore.Points > 0 { + if err := tx.Debug().Model(&cModel.UserCircle{}).Where("user_id = ? AND circle_id = ?", userID, chore.CircleID).Update("points", gorm.Expr("points + ?", chore.Points)).Error; err != nil { + return err + } + } return nil }) @@ -266,6 +277,7 @@ func (r *ChoreRepository) GetChoreDetailByID(c context.Context, choreID int, cir chores.assigned_to, chores.created_by, chores.priority, + chores.completion_window, recent_history.last_completed_date, recent_history.notes, recent_history.last_assigned_to as last_completed_by, @@ -301,3 +313,13 @@ func (r *ChoreRepository) ArchiveChore(c context.Context, choreID int, userID in func (r *ChoreRepository) UnarchiveChore(c context.Context, choreID int, userID int) error { return r.db.WithContext(c).Model(&chModel.Chore{}).Where("id = ? and created_by = ?", choreID, userID).Update("is_active", true).Error } + +func (r *ChoreRepository) GetChoresHistoryByUserID(c context.Context, userID int, circleID int, days int, includeCircle bool) ([]*chModel.ChoreHistory, error) { + + var chores []*chModel.ChoreHistory + since := time.Now().AddDate(0, 0, days*-1) + if err := r.db.WithContext(c).Where("completed_by = ? AND completed_at > ?", userID, since).Order("completed_at desc").Find(&chores).Error; err != nil { + return nil, err + } + return chores, nil +} diff --git a/internal/circle/handler.go b/internal/circle/handler.go index 26e9172..c597ac1 100644 --- a/internal/circle/handler.go +++ b/internal/circle/handler.go @@ -172,7 +172,7 @@ func (h *Handler) LeaveCircle(c *gin.Context) { } func handleUserLeavingCircle(h *Handler, c *gin.Context, leavingUser *uModel.User, orginalCircleID int) error { - userAssignedCircleChores, err := h.choreRepo.GetChores(c, leavingUser.CircleID, leavingUser.ID) + userAssignedCircleChores, err := h.choreRepo.GetChores(c, leavingUser.CircleID, leavingUser.ID, true) if err != nil { return err } diff --git a/internal/circle/model/model.go b/internal/circle/model/model.go index e731f1e..284c412 100644 --- a/internal/circle/model/model.go +++ b/internal/circle/model/model.go @@ -22,13 +22,15 @@ type CircleDetail struct { } type UserCircle struct { - ID int `json:"id" gorm:"primary_key"` // Unique identifier - UserID int `json:"userId" gorm:"column:user_id"` // User ID - CircleID int `json:"circleId" gorm:"column:circle_id"` // Circle ID - Role string `json:"role" gorm:"column:role"` // Role - IsActive bool `json:"isActive" gorm:"column:is_active;default:false"` - CreatedAt time.Time `json:"createdAt" gorm:"column:created_at"` // Created at - UpdatedAt time.Time `json:"updatedAt" gorm:"column:updated_at"` // Updated at + ID int `json:"id" gorm:"primary_key"` // Unique identifier + UserID int `json:"userId" gorm:"column:user_id"` // User ID + CircleID int `json:"circleId" gorm:"column:circle_id"` // Circle ID + Role string `json:"role" gorm:"column:role"` // Role + IsActive bool `json:"isActive" gorm:"column:is_active;default:false"` + CreatedAt time.Time `json:"createdAt" gorm:"column:created_at"` // Created at + UpdatedAt time.Time `json:"updatedAt" gorm:"column:updated_at"` // Updated at + Points int `json:"points" gorm:"column:points;default:0;not null"` // Points + PointsRedeemed int `json:"pointsRedeemed" gorm:"column:points_redeemed;default:0;not null"` // Points Redeemed } type UserCircleDetail struct {