Add Support to consume/redeem points

Support Creating task/chore via api
Add initial Description for tasks
This commit is contained in:
Mo Tarbin 2025-01-14 10:43:05 -05:00
parent 50b1357dfa
commit ac733343da
11 changed files with 368 additions and 66 deletions

View file

@ -9,6 +9,7 @@ import (
limiter "github.com/ulule/limiter/v3"
chModel "donetick.com/core/internal/chore/model"
uRepo "donetick.com/core/internal/user/repo"
)
@ -44,6 +45,46 @@ func (h *API) GetAllChores(c *gin.Context) {
c.JSON(200, chores)
}
func (h *API) CreateChore(c *gin.Context) {
var choreRequest chModel.ChoreReq
apiToken := c.GetHeader("secretkey")
if apiToken == "" {
c.JSON(401, gin.H{"error": "Unauthorized"})
return
}
user, err := h.userRepo.GetUserByToken(c, apiToken)
if err != nil {
c.JSON(401, gin.H{"error": "Unauthorized"})
return
}
if err := c.BindJSON(&choreRequest); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
chore := &chModel.Chore{
CreatedBy: user.ID,
CircleID: user.CircleID,
Name: choreRequest.Name,
IsRolling: choreRequest.IsRolling,
FrequencyType: choreRequest.FrequencyType,
Frequency: choreRequest.Frequency,
AssignStrategy: choreRequest.AssignStrategy,
AssignedTo: user.ID,
Assignees: []chModel.ChoreAssignees{{UserID: user.ID}},
Description: choreRequest.Description,
}
_, err = h.choreRepo.CreateChore(c, chore)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(200, chore)
}
func APIs(cfg *config.Config, api *API, r *gin.Engine, auth *jwt.GinJWTMiddleware, limiter *limiter.Limiter) {
thingsAPI := r.Group("eapi/v1/chore")

View file

@ -26,36 +26,6 @@ import (
"github.com/gin-gonic/gin"
)
type ThingTrigger struct {
ID int `json:"thingID" binding:"required"`
TriggerState string `json:"triggerState" binding:"required"`
Condition string `json:"condition"`
}
type LabelReq struct {
LabelID int `json:"id" binding:"required"`
}
type ChoreReq struct {
Name string `json:"name" binding:"required"`
FrequencyType chModel.FrequencyType `json:"frequencyType"`
ID int `json:"id"`
DueDate string `json:"dueDate"`
Assignees []chModel.ChoreAssignees `json:"assignees"`
AssignStrategy string `json:"assignStrategy" binding:"required"`
AssignedTo int `json:"assignedTo"`
IsRolling bool `json:"isRolling"`
IsActive bool `json:"isActive"`
Frequency int `json:"frequency"`
FrequencyMetadata *chModel.FrequencyMetadata `json:"frequencyMetadata"`
Notification bool `json:"notification"`
NotificationMetadata *chModel.NotificationMetadata `json:"notificationMetadata"`
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
circleRepo *cRepo.CircleRepository
@ -185,7 +155,7 @@ func (h *Handler) createChore(c *gin.Context) {
return
}
// Validate chore:
var choreReq ChoreReq
var choreReq chModel.ChoreReq
if err := c.ShouldBindJSON(&choreReq); err != nil {
log.Print(err)
c.JSON(400, gin.H{
@ -284,6 +254,7 @@ func (h *Handler) createChore(c *gin.Context) {
CircleID: currentUser.CircleID,
Points: choreReq.Points,
CompletionWindow: choreReq.CompletionWindow,
Description: choreReq.Description,
}
id, err := h.choreRepo.CreateChore(c, createdChore)
createdChore.ID = id
@ -302,18 +273,18 @@ func (h *Handler) createChore(c *gin.Context) {
UserID: assignee.UserID,
})
}
labelsV2 := make([]int, len(*choreReq.LabelsV2))
for i, label := range *choreReq.LabelsV2 {
labelsV2[i] = int(label.LabelID)
if choreReq.LabelsV2 != nil {
labelsV2 := make([]int, len(*choreReq.LabelsV2))
for i, label := range *choreReq.LabelsV2 {
labelsV2[i] = int(label.LabelID)
}
if err := h.lRepo.AssignLabelsToChore(c, createdChore.ID, currentUser.ID, currentUser.CircleID, labelsV2, []int{}); err != nil {
c.JSON(500, gin.H{
"error": "Error adding labels",
})
return
}
}
if err := h.lRepo.AssignLabelsToChore(c, createdChore.ID, currentUser.ID, currentUser.CircleID, labelsV2, []int{}); err != nil {
c.JSON(500, gin.H{
"error": "Error adding labels",
})
return
}
if err := h.choreRepo.UpdateChoreAssignees(c, choreAssignees); err != nil {
c.JSON(500, gin.H{
"error": "Error adding chore assignees",
@ -342,7 +313,7 @@ func (h *Handler) editChore(c *gin.Context) {
return
}
var choreReq ChoreReq
var choreReq chModel.ChoreReq
if err := c.ShouldBindJSON(&choreReq); err != nil {
log.Print(err)
c.JSON(400, gin.H{
@ -545,6 +516,7 @@ func (h *Handler) editChore(c *gin.Context) {
CreatedAt: oldChore.CreatedAt,
Points: choreReq.Points,
CompletionWindow: choreReq.CompletionWindow,
Description: choreReq.Description,
}
if err := h.choreRepo.UpsertChore(c, updatedChore); err != nil {
c.JSON(500, gin.H{
@ -589,7 +561,7 @@ func (h *Handler) editChore(c *gin.Context) {
})
}
func HandleThingAssociation(choreReq ChoreReq, h *Handler, c *gin.Context, currentUser *uModel.User) bool {
func HandleThingAssociation(choreReq chModel.ChoreReq, h *Handler, c *gin.Context, currentUser *uModel.User) bool {
if choreReq.ThingTrigger != nil {
thing, err := h.tRepo.GetThingByID(c, choreReq.ThingTrigger.ID)
if err != nil {
@ -744,6 +716,61 @@ func (h *Handler) updateAssignee(c *gin.Context) {
})
}
func (h *Handler) updateChoreStatus(c *gin.Context) {
type StatusReq struct {
Status *chModel.Status `json:"status" binding:"required"`
}
var statusReq StatusReq
if err := c.ShouldBindJSON(&statusReq); err != nil {
c.JSON(400, gin.H{
"error": "Invalid request",
})
}
rawID := c.Param("id")
id, err := strconv.Atoi(rawID)
if err != nil {
c.JSON(400, gin.H{
"error": "Invalid ID",
})
return
}
currentUser, ok := auth.CurrentUser(c)
if !ok {
c.JSON(500, gin.H{
"error": "Error getting current user",
})
return
}
chore, err := h.choreRepo.GetChore(c, id)
if err != nil {
c.JSON(500, gin.H{
"error": "Error getting chore",
})
return
}
if chore.CircleID != currentUser.CircleID {
c.JSON(403, gin.H{
"error": "You are not allowed to start this chore",
})
return
}
if err := h.choreRepo.UpdateChoreStatus(c, chore.ID, currentUser.ID, *statusReq.Status); err != nil {
c.JSON(500, gin.H{
"error": "Error starting chore",
})
return
}
c.JSON(200, gin.H{})
}
func (h *Handler) skipChore(c *gin.Context) {
rawID := c.Param("id")
id, err := strconv.Atoi(rawID)
@ -778,7 +805,7 @@ func (h *Handler) skipChore(c *gin.Context) {
}
nextAssigedTo := chore.AssignedTo
if err := h.choreRepo.CompleteChore(c, chore, nil, currentUser.ID, nextDueDate, nil, nextAssigedTo); err != nil {
if err := h.choreRepo.CompleteChore(c, chore, nil, currentUser.ID, nextDueDate, nil, nextAssigedTo, false); err != nil {
c.JSON(500, gin.H{
"error": "Error completing chore",
})
@ -1024,7 +1051,7 @@ func (h *Handler) completeChore(c *gin.Context) {
return
}
if err := h.choreRepo.CompleteChore(c, chore, additionalNotes, currentUser.ID, nextDueDate, &completedDate, nextAssignedTo); err != nil {
if err := h.choreRepo.CompleteChore(c, chore, additionalNotes, currentUser.ID, nextDueDate, &completedDate, nextAssignedTo, true); err != nil {
c.JSON(500, gin.H{
"error": "Error completing chore",
})
@ -1436,6 +1463,7 @@ func Routes(router *gin.Engine, h *Handler, auth *jwt.GinJWTMiddleware) {
choresRoutes.DELETE("/:id/history/:history_id", h.DeleteHistory)
choresRoutes.POST("/:id/do", h.completeChore)
choresRoutes.POST("/:id/skip", h.skipChore)
choresRoutes.PUT("/:id/status", h.updateChoreStatus)
choresRoutes.PUT("/:id/assignee", h.updateAssignee)
choresRoutes.PUT("/:id/dueDate", h.updateDueDate)
choresRoutes.PUT("/:id/archive", h.archiveChore)

View file

@ -3,7 +3,9 @@ package model
import (
"time"
lModel "donetick.com/core/internal/label/model"
tModel "donetick.com/core/internal/thing/model"
thingModel "donetick.com/core/internal/thing/model"
)
type FrequencyType string
@ -22,6 +24,16 @@ const (
FrequancyTypeNoRepeat FrequencyType = "no_repeat"
)
type AssignmentStrategy string
const (
AssignmentStrategyRandom AssignmentStrategy = "random"
AssignmentStrategyLeastAssigned AssignmentStrategy = "least_assigned"
AssignmentStrategyLeastCompleted AssignmentStrategy = "least_completed"
AssignmentStrategyKeepLastAssigned AssignmentStrategy = "keep_last_assigned"
AssignmentStrategyRandomExceptLastAssigned AssignmentStrategy = "random_except_last_assigned"
)
type Chore struct {
ID int `json:"id" gorm:"primary_key"`
Name string `json:"name" gorm:"column:name"` // Chore description
@ -32,7 +44,7 @@ type Chore struct {
IsRolling bool `json:"isRolling" gorm:"column:is_rolling"` // Whether the chore is rolling
AssignedTo int `json:"assignedTo" gorm:"column:assigned_to"` // Who the chore is assigned to
Assignees []ChoreAssignees `json:"assignees" gorm:"foreignkey:ChoreID;references:ID"` // Assignees of the chore
AssignStrategy string `json:"assignStrategy" gorm:"column:assign_strategy"` // How the chore is assigned
AssignStrategy AssignmentStrategy `json:"assignStrategy" gorm:"column:assign_strategy"` // How the chore is assigned
IsActive bool `json:"isActive" gorm:"column:is_active"` // Whether the chore is active
Notification bool `json:"notification" gorm:"column:notification"` // Whether the chore has notification
NotificationMetadata *string `json:"notificationMetadata" gorm:"column:notification_meta"` // Additional notification information
@ -44,11 +56,21 @@ type Chore struct {
CreatedBy int `json:"createdBy" gorm:"column:created_by"` // Who created the chore
UpdatedBy int `json:"updatedBy" gorm:"column:updated_by"` // Who last updated the chore
ThingChore *tModel.ThingChore `json:"thingChore" gorm:"foreignkey:chore_id;references:id;<-:false"` // ThingChore relationship
Status int `json:"status" gorm:"column:status"`
Status Status `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
Description *string `json:"description,omitempty" gorm:"type:text;column:description"` // Description of the chore
}
type Status int8
const (
ChoreStatusNoStatus Status = 0
ChoreStatusInProgress Status = 1
ChoreStatusPaused Status = 2
)
type ChoreAssignees struct {
ID int `json:"-" gorm:"primary_key"`
ChoreID int `json:"-" gorm:"column:chore_id;uniqueIndex:idx_chore_user"` // The chore this assignee is for
@ -69,11 +91,9 @@ type ChoreHistory struct {
type ChoreHistoryStatus int8
const (
ChoreHistoryStatusPending ChoreHistoryStatus = 0
ChoreHistoryStatusCompleted ChoreHistoryStatus = 1
ChoreHistoryStatusCompletedLate ChoreHistoryStatus = 2
ChoreHistoryStatusMissed ChoreHistoryStatus = 3
ChoreHistoryStatusSkipped ChoreHistoryStatus = 4
ChoreHistoryStatusPending ChoreHistoryStatus = 0
ChoreHistoryStatusCompleted ChoreHistoryStatus = 1
ChoreHistoryStatusSkipped ChoreHistoryStatus = 2
)
type FrequencyMetadata struct {
@ -126,3 +146,25 @@ type ChoreLabels struct {
UserID int `json:"userId" gorm:"primaryKey;autoIncrement:false;not null"`
Label Label
}
type ChoreReq struct {
Name string `json:"name" binding:"required"`
FrequencyType FrequencyType `json:"frequencyType"`
ID int `json:"id"`
DueDate string `json:"dueDate"`
Assignees []ChoreAssignees `json:"assignees"`
AssignStrategy AssignmentStrategy `json:"assignStrategy" binding:"required"`
AssignedTo int `json:"assignedTo"`
IsRolling bool `json:"isRolling"`
IsActive bool `json:"isActive"`
Frequency int `json:"frequency"`
FrequencyMetadata *FrequencyMetadata `json:"frequencyMetadata"`
Notification bool `json:"notification"`
NotificationMetadata *NotificationMetadata `json:"notificationMetadata"`
Labels []string `json:"labels"`
LabelsV2 *[]lModel.LabelReq `json:"labelsV2"`
ThingTrigger *thingModel.ThingTrigger `json:"thingTrigger"`
Points *int `json:"points"`
CompletionWindow *int `json:"completionWindow"`
Description *string `json:"description"`
}

View file

@ -85,15 +85,7 @@ func (r *ChoreRepository) IsChoreOwner(c context.Context, choreID int, userID in
return err
}
// func (r *ChoreRepository) ListChores(circleID int) ([]*chModel.Chore, error) {
// var chores []*Chore
// if err := r.db.WithContext(c).Find(&chores).Where("is_active = ?", true).Order("next_due_date").Error; err != nil {
// return nil, err
// }
// return chores, nil
// }
func (r *ChoreRepository) CompleteChore(c context.Context, chore *chModel.Chore, note *string, userID int, dueDate *time.Time, completedDate *time.Time, nextAssignedTo int) error {
func (r *ChoreRepository) CompleteChore(c context.Context, chore *chModel.Chore, note *string, userID int, dueDate *time.Time, completedDate *time.Time, nextAssignedTo int, applyPoints bool) error {
err := r.db.WithContext(c).Transaction(func(tx *gorm.DB) error {
ch := &chModel.ChoreHistory{
ChoreID: chore.ID,
@ -102,17 +94,18 @@ 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
}
updates := map[string]interface{}{}
updates["next_due_date"] = dueDate
updates["status"] = chModel.ChoreStatusNoStatus
if dueDate != nil {
updates["assigned_to"] = nextAssignedTo
} else {
// one time task
updates["is_active"] = false
}
// Perform the update operation once, using the prepared updates map.
@ -120,8 +113,8 @@ func (r *ChoreRepository) CompleteChore(c context.Context, chore *chModel.Chore,
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 {
if applyPoints && chore.Points != nil && *chore.Points > 0 {
if err := tx.Model(&cModel.UserCircle{}).Where("user_id = ? AND circle_id = ?", userID, chore.CircleID).Update("points", gorm.Expr("points + ?", chore.Points)).Error; err != nil {
return err
}
}
@ -323,3 +316,7 @@ func (r *ChoreRepository) GetChoresHistoryByUserID(c context.Context, userID int
}
return chores, nil
}
func (r *ChoreRepository) UpdateChoreStatus(c context.Context, choreID int, userId int, status chModel.Status) error {
return r.db.WithContext(c).Model(&chModel.Chore{}).Where("id = ?", choreID).Where("created_by = ? ", userId).Update("status", status).Error
}