Add subtask model and repository, implement webhook notification handling
fix Issue with Scheduler Support NotificationPlatformWebhook
This commit is contained in:
parent
41be361463
commit
8db572f1ec
13 changed files with 337 additions and 38 deletions
|
@ -16,6 +16,7 @@ import (
|
||||||
|
|
||||||
chModel "donetick.com/core/internal/chore/model"
|
chModel "donetick.com/core/internal/chore/model"
|
||||||
cRepo "donetick.com/core/internal/circle/repo"
|
cRepo "donetick.com/core/internal/circle/repo"
|
||||||
|
stRepo "donetick.com/core/internal/subtask/repo"
|
||||||
uRepo "donetick.com/core/internal/user/repo"
|
uRepo "donetick.com/core/internal/user/repo"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -25,15 +26,17 @@ type API struct {
|
||||||
circleRepo *cRepo.CircleRepository
|
circleRepo *cRepo.CircleRepository
|
||||||
nPlanner *nps.NotificationPlanner
|
nPlanner *nps.NotificationPlanner
|
||||||
eventProducer *events.EventsProducer
|
eventProducer *events.EventsProducer
|
||||||
|
stRepo *stRepo.SubTasksRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAPI(cr *chRepo.ChoreRepository, userRepo *uRepo.UserRepository, circleRepo *cRepo.CircleRepository, nPlanner *nps.NotificationPlanner, eventProducer *events.EventsProducer) *API {
|
func NewAPI(cr *chRepo.ChoreRepository, userRepo *uRepo.UserRepository, circleRepo *cRepo.CircleRepository, nPlanner *nps.NotificationPlanner, eventProducer *events.EventsProducer, stRepo *stRepo.SubTasksRepository) *API {
|
||||||
return &API{
|
return &API{
|
||||||
choreRepo: cr,
|
choreRepo: cr,
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
circleRepo: circleRepo,
|
circleRepo: circleRepo,
|
||||||
nPlanner: nPlanner,
|
nPlanner: nPlanner,
|
||||||
eventProducer: eventProducer,
|
eventProducer: eventProducer,
|
||||||
|
stRepo: stRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,6 +200,10 @@ func (h *API) CompleteChore(c *gin.Context) {
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if chore.SubTasks != nil && chore.FrequencyType != chModel.FrequencyTypeOnce {
|
||||||
|
h.stRepo.ResetSubtasksCompletion(c, chore.ID)
|
||||||
|
}
|
||||||
|
|
||||||
updatedChore, err := h.choreRepo.GetChore(c, choreID)
|
updatedChore, err := h.choreRepo.GetChore(c, choreID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, gin.H{
|
||||||
|
|
|
@ -20,6 +20,8 @@ import (
|
||||||
"donetick.com/core/internal/notifier"
|
"donetick.com/core/internal/notifier"
|
||||||
nRepo "donetick.com/core/internal/notifier/repo"
|
nRepo "donetick.com/core/internal/notifier/repo"
|
||||||
nps "donetick.com/core/internal/notifier/service"
|
nps "donetick.com/core/internal/notifier/service"
|
||||||
|
stModel "donetick.com/core/internal/subtask/model"
|
||||||
|
stRepo "donetick.com/core/internal/subtask/repo"
|
||||||
tRepo "donetick.com/core/internal/thing/repo"
|
tRepo "donetick.com/core/internal/thing/repo"
|
||||||
uModel "donetick.com/core/internal/user/model"
|
uModel "donetick.com/core/internal/user/model"
|
||||||
"donetick.com/core/logging"
|
"donetick.com/core/logging"
|
||||||
|
@ -36,11 +38,12 @@ type Handler struct {
|
||||||
tRepo *tRepo.ThingRepository
|
tRepo *tRepo.ThingRepository
|
||||||
lRepo *lRepo.LabelRepository
|
lRepo *lRepo.LabelRepository
|
||||||
eventProducer *events.EventsProducer
|
eventProducer *events.EventsProducer
|
||||||
|
stRepo *stRepo.SubTasksRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(cr *chRepo.ChoreRepository, circleRepo *cRepo.CircleRepository, nt *notifier.Notifier,
|
func NewHandler(cr *chRepo.ChoreRepository, circleRepo *cRepo.CircleRepository, nt *notifier.Notifier,
|
||||||
np *nps.NotificationPlanner, nRepo *nRepo.NotificationRepository, tRepo *tRepo.ThingRepository, lRepo *lRepo.LabelRepository,
|
np *nps.NotificationPlanner, nRepo *nRepo.NotificationRepository, tRepo *tRepo.ThingRepository, lRepo *lRepo.LabelRepository,
|
||||||
ep *events.EventsProducer) *Handler {
|
ep *events.EventsProducer, stRepo *stRepo.SubTasksRepository) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
choreRepo: cr,
|
choreRepo: cr,
|
||||||
circleRepo: circleRepo,
|
circleRepo: circleRepo,
|
||||||
|
@ -50,6 +53,7 @@ func NewHandler(cr *chRepo.ChoreRepository, circleRepo *cRepo.CircleRepository,
|
||||||
tRepo: tRepo,
|
tRepo: tRepo,
|
||||||
lRepo: lRepo,
|
lRepo: lRepo,
|
||||||
eventProducer: ep,
|
eventProducer: ep,
|
||||||
|
stRepo: stRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,6 +263,7 @@ func (h *Handler) createChore(c *gin.Context) {
|
||||||
Points: choreReq.Points,
|
Points: choreReq.Points,
|
||||||
CompletionWindow: choreReq.CompletionWindow,
|
CompletionWindow: choreReq.CompletionWindow,
|
||||||
Description: choreReq.Description,
|
Description: choreReq.Description,
|
||||||
|
SubTasks: choreReq.SubTasks,
|
||||||
}
|
}
|
||||||
id, err := h.choreRepo.CreateChore(c, createdChore)
|
id, err := h.choreRepo.CreateChore(c, createdChore)
|
||||||
createdChore.ID = id
|
createdChore.ID = id
|
||||||
|
@ -270,6 +275,10 @@ func (h *Handler) createChore(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if choreReq.SubTasks != nil {
|
||||||
|
h.stRepo.CreateSubtasks(c, nil, choreReq.SubTasks, createdChore.ID)
|
||||||
|
}
|
||||||
|
|
||||||
var choreAssignees []*chModel.ChoreAssignees
|
var choreAssignees []*chModel.ChoreAssignees
|
||||||
for _, assignee := range choreReq.Assignees {
|
for _, assignee := range choreReq.Assignees {
|
||||||
choreAssignees = append(choreAssignees, &chModel.ChoreAssignees{
|
choreAssignees = append(choreAssignees, &chModel.ChoreAssignees{
|
||||||
|
@ -521,6 +530,7 @@ func (h *Handler) editChore(c *gin.Context) {
|
||||||
Points: choreReq.Points,
|
Points: choreReq.Points,
|
||||||
CompletionWindow: choreReq.CompletionWindow,
|
CompletionWindow: choreReq.CompletionWindow,
|
||||||
Description: choreReq.Description,
|
Description: choreReq.Description,
|
||||||
|
Priority: choreReq.Priority,
|
||||||
}
|
}
|
||||||
if err := h.choreRepo.UpsertChore(c, updatedChore); err != nil {
|
if err := h.choreRepo.UpsertChore(c, updatedChore); err != nil {
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, gin.H{
|
||||||
|
@ -528,6 +538,54 @@ func (h *Handler) editChore(c *gin.Context) {
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if choreReq.SubTasks != nil {
|
||||||
|
ToBeRemoved := []stModel.SubTask{}
|
||||||
|
ToBeAdded := []stModel.SubTask{}
|
||||||
|
if oldChore.SubTasks == nil {
|
||||||
|
oldChore.SubTasks = &[]stModel.SubTask{}
|
||||||
|
}
|
||||||
|
if choreReq.SubTasks == nil {
|
||||||
|
choreReq.SubTasks = &[]stModel.SubTask{}
|
||||||
|
}
|
||||||
|
for _, existedSubTask := range *oldChore.SubTasks {
|
||||||
|
found := false
|
||||||
|
for _, newSubTask := range *choreReq.SubTasks {
|
||||||
|
if existedSubTask.ID == newSubTask.ID {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
ToBeRemoved = append(ToBeRemoved, existedSubTask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, newSubTask := range *choreReq.SubTasks {
|
||||||
|
found := false
|
||||||
|
newSubTask.ChoreID = oldChore.ID
|
||||||
|
|
||||||
|
for _, existedSubTask := range *oldChore.SubTasks {
|
||||||
|
if existedSubTask.ID == newSubTask.ID {
|
||||||
|
if existedSubTask.Name != newSubTask.Name || existedSubTask.OrderID != newSubTask.OrderID {
|
||||||
|
// there is a change in the subtask, update it
|
||||||
|
break
|
||||||
|
}
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
ToBeAdded = append(ToBeAdded, newSubTask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := h.stRepo.UpdateSubtask(c, oldChore.ID, ToBeRemoved, ToBeAdded); err != nil {
|
||||||
|
c.JSON(500, gin.H{
|
||||||
|
"error": "Error adding subtasks",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(choreAssigneesToAdd) > 0 {
|
if len(choreAssigneesToAdd) > 0 {
|
||||||
err = h.choreRepo.UpdateChoreAssignees(c, choreAssigneesToAdd)
|
err = h.choreRepo.UpdateChoreAssignees(c, choreAssigneesToAdd)
|
||||||
|
|
||||||
|
@ -1077,6 +1135,10 @@ func (h *Handler) completeChore(c *gin.Context) {
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if updatedChore.SubTasks != nil && updatedChore.FrequencyType != chModel.FrequencyTypeOnce {
|
||||||
|
h.stRepo.ResetSubtasksCompletion(c, updatedChore.ID)
|
||||||
|
}
|
||||||
|
|
||||||
// go func() {
|
// go func() {
|
||||||
|
|
||||||
// h.notifier.SendChoreCompletion(c, chore, currentUser)
|
// h.notifier.SendChoreCompletion(c, chore, currentUser)
|
||||||
|
@ -1355,6 +1417,76 @@ func (h *Handler) DeleteHistory(c *gin.Context) {
|
||||||
"message": "History deleted successfully",
|
"message": "History deleted successfully",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) UpdateSubtaskCompletedAt(c *gin.Context) {
|
||||||
|
currentUser, ok := auth.CurrentUser(c)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(500, gin.H{
|
||||||
|
"error": "Error getting current user",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawID := c.Param("id")
|
||||||
|
choreID, err := strconv.Atoi(rawID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": "Invalid Chore ID",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubtaskReq struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
ChoreID int `json:"choreId"`
|
||||||
|
CompletedAt *time.Time `json:"completedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var req SubtaskReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": "Invalid request",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chore, err := h.choreRepo.GetChore(c, choreID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, gin.H{
|
||||||
|
"error": "Error getting chore",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !chore.CanComplete(currentUser.ID) {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": "User is not assigned to chore",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var completedAt *time.Time
|
||||||
|
if req.CompletedAt != nil {
|
||||||
|
completedAt = req.CompletedAt
|
||||||
|
}
|
||||||
|
err = h.stRepo.UpdateSubTaskStatus(c, currentUser.ID, req.ID, completedAt)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, gin.H{
|
||||||
|
"error": "Error getting subtask",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.eventProducer.SubtaskUpdated(c, currentUser.WebhookURL,
|
||||||
|
&stModel.SubTask{
|
||||||
|
ID: req.ID,
|
||||||
|
ChoreID: req.ChoreID,
|
||||||
|
CompletedAt: completedAt,
|
||||||
|
CompletedBy: currentUser.ID,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
c.JSON(200, gin.H{})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func checkNextAssignee(chore *chModel.Chore, choresHistory []*chModel.ChoreHistory, performerID int) (int, error) {
|
func checkNextAssignee(chore *chModel.Chore, choresHistory []*chModel.ChoreHistory, performerID int) (int, error) {
|
||||||
// copy the history to avoid modifying the original:
|
// copy the history to avoid modifying the original:
|
||||||
history := make([]*chModel.ChoreHistory, len(choresHistory))
|
history := make([]*chModel.ChoreHistory, len(choresHistory))
|
||||||
|
@ -1470,6 +1602,7 @@ func Routes(router *gin.Engine, h *Handler, auth *jwt.GinJWTMiddleware) {
|
||||||
choresRoutes.PUT("/:id/priority", h.updatePriority)
|
choresRoutes.PUT("/:id/priority", h.updatePriority)
|
||||||
choresRoutes.POST("/", h.createChore)
|
choresRoutes.POST("/", h.createChore)
|
||||||
choresRoutes.GET("/:id", h.getChore)
|
choresRoutes.GET("/:id", h.getChore)
|
||||||
|
choresRoutes.PUT("/:id/subtask", h.UpdateSubtaskCompletedAt)
|
||||||
choresRoutes.GET("/:id/details", h.GetChoreDetail)
|
choresRoutes.GET("/:id/details", h.GetChoreDetail)
|
||||||
choresRoutes.GET("/:id/history", h.GetChoreHistory)
|
choresRoutes.GET("/:id/history", h.GetChoreHistory)
|
||||||
choresRoutes.PUT("/:id/history/:history_id", h.ModifyHistory)
|
choresRoutes.PUT("/:id/history/:history_id", h.ModifyHistory)
|
||||||
|
|
|
@ -5,8 +5,8 @@ import (
|
||||||
|
|
||||||
cModel "donetick.com/core/internal/circle/model"
|
cModel "donetick.com/core/internal/circle/model"
|
||||||
lModel "donetick.com/core/internal/label/model"
|
lModel "donetick.com/core/internal/label/model"
|
||||||
|
stModel "donetick.com/core/internal/subtask/model"
|
||||||
tModel "donetick.com/core/internal/thing/model"
|
tModel "donetick.com/core/internal/thing/model"
|
||||||
thingModel "donetick.com/core/internal/thing/model"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type FrequencyType string
|
type FrequencyType string
|
||||||
|
@ -62,6 +62,7 @@ type Chore struct {
|
||||||
CompletionWindow *int `json:"completionWindow,omitempty" gorm:"column:completion_window"` // Number seconds before the chore is due that it can be completed
|
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
|
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
|
Description *string `json:"description,omitempty" gorm:"type:text;column:description"` // Description of the chore
|
||||||
|
SubTasks *[]stModel.SubTask `json:"subTasks,omitempty" gorm:"foreignkey:ChoreID;references:ID"` // Subtasks for the chore
|
||||||
}
|
}
|
||||||
|
|
||||||
type Status int8
|
type Status int8
|
||||||
|
@ -119,19 +120,20 @@ type Tag struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChoreDetail struct {
|
type ChoreDetail struct {
|
||||||
ID int `json:"id" gorm:"column:id"`
|
ID int `json:"id" gorm:"column:id"`
|
||||||
Name string `json:"name" gorm:"column:name"`
|
Name string `json:"name" gorm:"column:name"`
|
||||||
Description *string `json:"description" gorm:"column:description"`
|
Description *string `json:"description" gorm:"column:description"`
|
||||||
FrequencyType string `json:"frequencyType" gorm:"column:frequency_type"`
|
FrequencyType string `json:"frequencyType" gorm:"column:frequency_type"`
|
||||||
NextDueDate *time.Time `json:"nextDueDate" gorm:"column:next_due_date"`
|
NextDueDate *time.Time `json:"nextDueDate" gorm:"column:next_due_date"`
|
||||||
AssignedTo int `json:"assignedTo" gorm:"column:assigned_to"`
|
AssignedTo int `json:"assignedTo" gorm:"column:assigned_to"`
|
||||||
LastCompletedDate *time.Time `json:"lastCompletedDate" gorm:"column:last_completed_date"`
|
LastCompletedDate *time.Time `json:"lastCompletedDate" gorm:"column:last_completed_date"`
|
||||||
LastCompletedBy *int `json:"lastCompletedBy" gorm:"column:last_completed_by"`
|
LastCompletedBy *int `json:"lastCompletedBy" gorm:"column:last_completed_by"`
|
||||||
TotalCompletedCount int `json:"totalCompletedCount" gorm:"column:total_completed"`
|
TotalCompletedCount int `json:"totalCompletedCount" gorm:"column:total_completed"`
|
||||||
Priority int `json:"priority" gorm:"column:priority"`
|
Priority int `json:"priority" gorm:"column:priority"`
|
||||||
Notes *string `json:"notes" gorm:"column:notes"`
|
Notes *string `json:"notes" gorm:"column:notes"`
|
||||||
CreatedBy int `json:"createdBy" gorm:"column:created_by"`
|
CreatedBy int `json:"createdBy" gorm:"column:created_by"`
|
||||||
CompletionWindow *int `json:"completionWindow,omitempty" gorm:"column:completion_window"`
|
CompletionWindow *int `json:"completionWindow,omitempty" gorm:"column:completion_window"`
|
||||||
|
Subtasks *[]stModel.SubTask `json:"subTasks,omitempty" gorm:"foreignkey:ChoreID;references:ID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Label struct {
|
type Label struct {
|
||||||
|
@ -150,25 +152,27 @@ type ChoreLabels struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChoreReq struct {
|
type ChoreReq struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
FrequencyType FrequencyType `json:"frequencyType"`
|
FrequencyType FrequencyType `json:"frequencyType"`
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
DueDate string `json:"dueDate"`
|
DueDate string `json:"dueDate"`
|
||||||
Assignees []ChoreAssignees `json:"assignees"`
|
Assignees []ChoreAssignees `json:"assignees"`
|
||||||
AssignStrategy AssignmentStrategy `json:"assignStrategy" binding:"required"`
|
AssignStrategy AssignmentStrategy `json:"assignStrategy" binding:"required"`
|
||||||
AssignedTo int `json:"assignedTo"`
|
AssignedTo int `json:"assignedTo"`
|
||||||
IsRolling bool `json:"isRolling"`
|
IsRolling bool `json:"isRolling"`
|
||||||
IsActive bool `json:"isActive"`
|
IsActive bool `json:"isActive"`
|
||||||
Frequency int `json:"frequency"`
|
Frequency int `json:"frequency"`
|
||||||
FrequencyMetadata *FrequencyMetadata `json:"frequencyMetadata"`
|
FrequencyMetadata *FrequencyMetadata `json:"frequencyMetadata"`
|
||||||
Notification bool `json:"notification"`
|
Notification bool `json:"notification"`
|
||||||
NotificationMetadata *NotificationMetadata `json:"notificationMetadata"`
|
NotificationMetadata *NotificationMetadata `json:"notificationMetadata"`
|
||||||
Labels []string `json:"labels"`
|
Labels []string `json:"labels"`
|
||||||
LabelsV2 *[]lModel.LabelReq `json:"labelsV2"`
|
LabelsV2 *[]lModel.LabelReq `json:"labelsV2"`
|
||||||
ThingTrigger *thingModel.ThingTrigger `json:"thingTrigger"`
|
ThingTrigger *tModel.ThingTrigger `json:"thingTrigger"`
|
||||||
Points *int `json:"points"`
|
Points *int `json:"points"`
|
||||||
CompletionWindow *int `json:"completionWindow"`
|
CompletionWindow *int `json:"completionWindow"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
|
Priority int `json:"priority"`
|
||||||
|
SubTasks *[]stModel.SubTask `json:"subTasks"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chore) CanEdit(userID int, circleUsers []*cModel.UserCircleDetail) bool {
|
func (c *Chore) CanEdit(userID int, circleUsers []*cModel.UserCircleDetail) bool {
|
||||||
|
|
|
@ -44,7 +44,7 @@ func (r *ChoreRepository) CreateChore(c context.Context, chore *chModel.Chore) (
|
||||||
|
|
||||||
func (r *ChoreRepository) GetChore(c context.Context, choreID int) (*chModel.Chore, error) {
|
func (r *ChoreRepository) GetChore(c context.Context, choreID int) (*chModel.Chore, error) {
|
||||||
var chore chModel.Chore
|
var chore chModel.Chore
|
||||||
if err := r.db.Debug().WithContext(c).Model(&chModel.Chore{}).Preload("Assignees").Preload("ThingChore").Preload("LabelsV2").First(&chore, choreID).Error; err != nil {
|
if err := r.db.Debug().WithContext(c).Model(&chModel.Chore{}).Preload("SubTasks").Preload("Assignees").Preload("ThingChore").Preload("LabelsV2").First(&chore, choreID).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &chore, nil
|
return &chore, nil
|
||||||
|
@ -279,6 +279,7 @@ func (r *ChoreRepository) GetChoreDetailByID(c context.Context, choreID int, cir
|
||||||
var choreDetail chModel.ChoreDetail
|
var choreDetail chModel.ChoreDetail
|
||||||
if err := r.db.WithContext(c).
|
if err := r.db.WithContext(c).
|
||||||
Table("chores").
|
Table("chores").
|
||||||
|
Preload("Subtasks").
|
||||||
Select(`
|
Select(`
|
||||||
chores.id,
|
chores.id,
|
||||||
chores.name,
|
chores.name,
|
||||||
|
|
|
@ -91,6 +91,16 @@ func scheduleNextDueDate(chore *chModel.Chore, completedDate time.Time) (*time.T
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("no matching day of the week found")
|
return nil, fmt.Errorf("no matching day of the week found")
|
||||||
case "day_of_the_month":
|
case "day_of_the_month":
|
||||||
|
// for day of the month we need to pick the highest between completed date and next due date
|
||||||
|
// when the chore is rolling. i keep forgetting so am writing a detail comment here:
|
||||||
|
// if task due every 15 of jan, and you completed it on the 13 of jan( before the due date ) if we schedule from due date
|
||||||
|
// we will go back to 15 of jan. so we need to pick the highest between the two dates specifically for day of the month
|
||||||
|
if chore.IsRolling && chore.NextDueDate != nil {
|
||||||
|
secondAfterDueDate := chore.NextDueDate.UTC().Add(time.Second)
|
||||||
|
if completedDate.Before(secondAfterDueDate) {
|
||||||
|
baseDate = secondAfterDueDate
|
||||||
|
}
|
||||||
|
}
|
||||||
if len(frequencyMetadata.Months) == 0 {
|
if len(frequencyMetadata.Months) == 0 {
|
||||||
return nil, fmt.Errorf("day_of_the_month requires at least one month")
|
return nil, fmt.Errorf("day_of_the_month requires at least one month")
|
||||||
}
|
}
|
||||||
|
@ -101,7 +111,13 @@ func scheduleNextDueDate(chore *chModel.Chore, completedDate time.Time) (*time.T
|
||||||
|
|
||||||
// Find the next valid day of the month, considering the year
|
// Find the next valid day of the month, considering the year
|
||||||
currentMonth := int(baseDate.Month())
|
currentMonth := int(baseDate.Month())
|
||||||
for i := 0; i < 12; i++ { // Start from 0 to check the current month first
|
|
||||||
|
var startFrom int
|
||||||
|
if chore.NextDueDate != nil && baseDate.Month() == chore.NextDueDate.Month() {
|
||||||
|
startFrom = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := startFrom; i < 12+startFrom; i++ { // Start from 0 to check the current month first
|
||||||
nextDueDate := baseDate.AddDate(0, i, 0)
|
nextDueDate := baseDate.AddDate(0, i, 0)
|
||||||
nextMonth := (currentMonth + i) % 12 // Use modulo to cycle through months
|
nextMonth := (currentMonth + i) % 12 // Use modulo to cycle through months
|
||||||
if nextMonth == 0 {
|
if nextMonth == 0 {
|
||||||
|
|
|
@ -224,11 +224,25 @@ func TestScheduleNextDueDateDayOfMonth(t *testing.T) {
|
||||||
chore: chModel.Chore{
|
chore: chModel.Chore{
|
||||||
FrequencyType: chModel.FrequencyTypeDayOfTheMonth,
|
FrequencyType: chModel.FrequencyTypeDayOfTheMonth,
|
||||||
Frequency: 15,
|
Frequency: 15,
|
||||||
|
IsRolling: true,
|
||||||
FrequencyMetadata: jsonPtr(`{ "unit": "days", "time": "2025-01-20T18:00:00-05:00", "days": [], "months": [ "january" ] }`),
|
FrequencyMetadata: jsonPtr(`{ "unit": "days", "time": "2025-01-20T18:00:00-05:00", "days": [], "months": [ "january" ] }`),
|
||||||
},
|
},
|
||||||
completedDate: now.AddDate(1, 1, 0),
|
completedDate: now.AddDate(1, 1, 0),
|
||||||
want: timePtr(time.Date(2027, 1, 15, 18, 0, 0, 0, location)),
|
want: timePtr(time.Date(2027, 1, 15, 18, 0, 0, 0, location)),
|
||||||
},
|
},
|
||||||
|
// test if completed before the 15th of the month:
|
||||||
|
{
|
||||||
|
name: "Day of the month - 15th of January(isRolling)(Completed before due date)",
|
||||||
|
chore: chModel.Chore{
|
||||||
|
NextDueDate: timePtr(time.Date(2025, 1, 15, 18, 0, 0, 0, location)),
|
||||||
|
FrequencyType: chModel.FrequencyTypeDayOfTheMonth,
|
||||||
|
Frequency: 15,
|
||||||
|
IsRolling: true,
|
||||||
|
FrequencyMetadata: jsonPtr(`{ "unit": "days", "time": "2025-01-20T18:00:00-05:00", "days": [], "months": [ "january" ] }`),
|
||||||
|
},
|
||||||
|
completedDate: now.AddDate(0, 0, 2),
|
||||||
|
want: timePtr(time.Date(2026, 1, 15, 18, 0, 0, 0, location)),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
executeTestTable(t, tests)
|
executeTestTable(t, tests)
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
cModel "donetick.com/core/internal/circle/model"
|
cModel "donetick.com/core/internal/circle/model"
|
||||||
nModel "donetick.com/core/internal/notifier/model"
|
nModel "donetick.com/core/internal/notifier/model"
|
||||||
pModel "donetick.com/core/internal/points"
|
pModel "donetick.com/core/internal/points"
|
||||||
|
stModel "donetick.com/core/internal/subtask/model"
|
||||||
tModel "donetick.com/core/internal/thing/model"
|
tModel "donetick.com/core/internal/thing/model"
|
||||||
uModel "donetick.com/core/internal/user/model" // Pure go SQLite driver, checkout https://github.com/glebarez/sqlite for details
|
uModel "donetick.com/core/internal/user/model" // Pure go SQLite driver, checkout https://github.com/glebarez/sqlite for details
|
||||||
migrations "donetick.com/core/migrations"
|
migrations "donetick.com/core/migrations"
|
||||||
|
@ -37,6 +38,7 @@ func Migration(db *gorm.DB) error {
|
||||||
chModel.ChoreLabels{},
|
chModel.ChoreLabels{},
|
||||||
migrations.Migration{},
|
migrations.Migration{},
|
||||||
pModel.PointsHistory{},
|
pModel.PointsHistory{},
|
||||||
|
stModel.SubTask{},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,8 @@ const (
|
||||||
// EventTypeTaskCreated EventType = "task.created"
|
// EventTypeTaskCreated EventType = "task.created"
|
||||||
EventTypeTaskReminder EventType = "task.reminder"
|
EventTypeTaskReminder EventType = "task.reminder"
|
||||||
// EventTypeTaskUpdated EventType = "task.updated"
|
// EventTypeTaskUpdated EventType = "task.updated"
|
||||||
EventTypeTaskCompleted EventType = "task.completed"
|
EventTypeTaskCompleted EventType = "task.completed"
|
||||||
|
EventTypeSubTaskCompleted EventType = "subtask.completed"
|
||||||
// EventTypeTaskReassigned EventType = "task.reassigned"
|
// EventTypeTaskReassigned EventType = "task.reassigned"
|
||||||
EventTypeTaskSkipped EventType = "task.skipped"
|
EventTypeTaskSkipped EventType = "task.skipped"
|
||||||
EventTypeThingChanged EventType = "thing.changed"
|
EventTypeThingChanged EventType = "thing.changed"
|
||||||
|
@ -172,3 +173,16 @@ func (p *EventsProducer) ThingsUpdated(ctx context.Context, url string, data int
|
||||||
Data: data,
|
Data: data,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *EventsProducer) SubtaskUpdated(ctx context.Context, url *string, data interface{}) {
|
||||||
|
if url == nil {
|
||||||
|
p.logger.Debug("No subscribers for circle, skipping webhook")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.publishEvent(Event{
|
||||||
|
URL: *url,
|
||||||
|
Type: EventTypeSubTaskCompleted,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ const (
|
||||||
NotificationPlatformNone NotificationPlatform = iota
|
NotificationPlatformNone NotificationPlatform = iota
|
||||||
NotificationPlatformTelegram
|
NotificationPlatformTelegram
|
||||||
NotificationPlatformPushover
|
NotificationPlatformPushover
|
||||||
|
NotificationPlatformWebhook
|
||||||
)
|
)
|
||||||
|
|
||||||
type JSONB map[string]interface{}
|
type JSONB map[string]interface{}
|
||||||
|
|
|
@ -41,6 +41,15 @@ func (n *Notifier) SendNotification(c context.Context, notification *nModel.Noti
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
err = n.Pushover.SendNotification(c, notification)
|
err = n.Pushover.SendNotification(c, notification)
|
||||||
|
case nModel.NotificationPlatformWebhook:
|
||||||
|
// TODO: Implement webhook notification
|
||||||
|
// currently we have eventProducer to send events always as a webhook
|
||||||
|
// if NotificationPlatform is selected. this a case to catch
|
||||||
|
// when we only want to send a webhook
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Error("Unknown notification type", "type", notification.TypeID)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Failed to send notification", "err", err)
|
log.Error("Failed to send notification", "err", err)
|
||||||
|
|
12
internal/subtask/model/model.go
Normal file
12
internal/subtask/model/model.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type SubTask struct {
|
||||||
|
ID int `json:"id" gorm:"primary_key"`
|
||||||
|
ChoreID int `json:"-" gorm:"column:chore_id;index"`
|
||||||
|
OrderID int8 `json:"orderId" gorm:"column:order_id"`
|
||||||
|
Name string `json:"name" gorm:"column:name"`
|
||||||
|
CompletedAt *time.Time `json:"completedAt" gorm:"column:completed_at"`
|
||||||
|
CompletedBy int `json:"completedBy" gorm:"column:completed_by"`
|
||||||
|
}
|
84
internal/subtask/repo/repository.go
Normal file
84
internal/subtask/repo/repository.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
stModel "donetick.com/core/internal/subtask/model"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SubTasksRepository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSubTasksRepository(db *gorm.DB) *SubTasksRepository {
|
||||||
|
return &SubTasksRepository{db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SubTasksRepository) CreateSubtasks(c context.Context, tx *gorm.DB, subtasks *[]stModel.SubTask, choreID int) error {
|
||||||
|
if tx != nil {
|
||||||
|
return tx.Model(&stModel.SubTask{}).Save(subtasks).Error
|
||||||
|
}
|
||||||
|
return r.db.WithContext(c).Save(subtasks).Error
|
||||||
|
}
|
||||||
|
func (r *SubTasksRepository) UpdateSubtask(c context.Context, choreId int, toBeRemove []stModel.SubTask, toBeAdd []stModel.SubTask) error {
|
||||||
|
return r.db.WithContext(c).Transaction(func(tx *gorm.DB) error {
|
||||||
|
if len(toBeRemove) == 0 && len(toBeAdd) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(toBeRemove) > 0 {
|
||||||
|
if err := tx.Delete(toBeRemove).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(toBeAdd) > 0 {
|
||||||
|
var insertions []stModel.SubTask
|
||||||
|
var updates []stModel.SubTask
|
||||||
|
for _, subtask := range toBeAdd {
|
||||||
|
if subtask.ID == 0 {
|
||||||
|
insertions = append(insertions, subtask)
|
||||||
|
} else {
|
||||||
|
updates = append(updates, subtask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(insertions) > 0 {
|
||||||
|
if err := tx.Create(&insertions).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(updates) > 0 {
|
||||||
|
for _, subtask := range updates {
|
||||||
|
if err := tx.Model(&stModel.SubTask{}).Where("chore_id = ? AND id = ?", choreId, subtask.ID).Updates(subtask).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SubTasksRepository) DeleteSubtask(c context.Context, tx *gorm.DB, subtaskID int) error {
|
||||||
|
if tx != nil {
|
||||||
|
return tx.Delete(&stModel.SubTask{}, subtaskID).Error
|
||||||
|
}
|
||||||
|
return r.db.WithContext(c).Delete(&stModel.SubTask{}, subtaskID).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SubTasksRepository) UpdateSubTaskStatus(c context.Context, userID int, subtaskID int, completedAt *time.Time) error {
|
||||||
|
return r.db.Model(&stModel.SubTask{}).Where("id = ?", subtaskID).Updates(map[string]interface{}{
|
||||||
|
"completed_at": completedAt,
|
||||||
|
"completed_by": userID,
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SubTasksRepository) ResetSubtasksCompletion(c context.Context, choreID int) error {
|
||||||
|
return r.db.Model(&stModel.SubTask{}).Where("chore_id = ?", choreID).Updates(map[string]interface{}{
|
||||||
|
"completed_at": nil,
|
||||||
|
"completed_by": nil,
|
||||||
|
}).Error
|
||||||
|
}
|
2
main.go
2
main.go
|
@ -26,6 +26,7 @@ import (
|
||||||
label "donetick.com/core/internal/label"
|
label "donetick.com/core/internal/label"
|
||||||
lRepo "donetick.com/core/internal/label/repo"
|
lRepo "donetick.com/core/internal/label/repo"
|
||||||
"donetick.com/core/internal/resource"
|
"donetick.com/core/internal/resource"
|
||||||
|
spRepo "donetick.com/core/internal/subtask/repo"
|
||||||
|
|
||||||
notifier "donetick.com/core/internal/notifier"
|
notifier "donetick.com/core/internal/notifier"
|
||||||
nRepo "donetick.com/core/internal/notifier/repo"
|
nRepo "donetick.com/core/internal/notifier/repo"
|
||||||
|
@ -89,6 +90,7 @@ func main() {
|
||||||
|
|
||||||
// points
|
// points
|
||||||
fx.Provide(pRepo.NewPointsRepository),
|
fx.Provide(pRepo.NewPointsRepository),
|
||||||
|
fx.Provide(spRepo.NewSubTasksRepository),
|
||||||
|
|
||||||
// Labels:
|
// Labels:
|
||||||
fx.Provide(lRepo.NewLabelRepository),
|
fx.Provide(lRepo.NewLabelRepository),
|
||||||
|
|
Loading…
Add table
Reference in a new issue