From 8db572f1ec2dc0b05bac0f739cf865c19650c727 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Tue, 25 Feb 2025 23:56:49 -0500 Subject: [PATCH] Add subtask model and repository, implement webhook notification handling fix Issue with Scheduler Support NotificationPlatformWebhook --- internal/chore/api.go | 9 +- internal/chore/handler.go | 135 +++++++++++++++++++++++++++- internal/chore/model/model.go | 70 ++++++++------- internal/chore/repo/repository.go | 3 +- internal/chore/scheduler.go | 18 +++- internal/chore/scheduler_test.go | 14 +++ internal/database/migration.go | 2 + internal/events/producer.go | 16 +++- internal/notifier/model/model.go | 1 + internal/notifier/notifier.go | 9 ++ internal/subtask/model/model.go | 12 +++ internal/subtask/repo/repository.go | 84 +++++++++++++++++ main.go | 2 + 13 files changed, 337 insertions(+), 38 deletions(-) create mode 100644 internal/subtask/model/model.go create mode 100644 internal/subtask/repo/repository.go diff --git a/internal/chore/api.go b/internal/chore/api.go index c17f228..1520650 100644 --- a/internal/chore/api.go +++ b/internal/chore/api.go @@ -16,6 +16,7 @@ import ( chModel "donetick.com/core/internal/chore/model" cRepo "donetick.com/core/internal/circle/repo" + stRepo "donetick.com/core/internal/subtask/repo" uRepo "donetick.com/core/internal/user/repo" ) @@ -25,15 +26,17 @@ type API struct { circleRepo *cRepo.CircleRepository nPlanner *nps.NotificationPlanner 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{ choreRepo: cr, userRepo: userRepo, circleRepo: circleRepo, nPlanner: nPlanner, eventProducer: eventProducer, + stRepo: stRepo, } } @@ -197,6 +200,10 @@ func (h *API) CompleteChore(c *gin.Context) { }) return } + if chore.SubTasks != nil && chore.FrequencyType != chModel.FrequencyTypeOnce { + h.stRepo.ResetSubtasksCompletion(c, chore.ID) + } + updatedChore, err := h.choreRepo.GetChore(c, choreID) if err != nil { c.JSON(500, gin.H{ diff --git a/internal/chore/handler.go b/internal/chore/handler.go index 9868cf5..cf62ece 100644 --- a/internal/chore/handler.go +++ b/internal/chore/handler.go @@ -20,6 +20,8 @@ import ( "donetick.com/core/internal/notifier" nRepo "donetick.com/core/internal/notifier/repo" 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" uModel "donetick.com/core/internal/user/model" "donetick.com/core/logging" @@ -36,11 +38,12 @@ type Handler struct { tRepo *tRepo.ThingRepository lRepo *lRepo.LabelRepository eventProducer *events.EventsProducer + stRepo *stRepo.SubTasksRepository } func NewHandler(cr *chRepo.ChoreRepository, circleRepo *cRepo.CircleRepository, nt *notifier.Notifier, 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{ choreRepo: cr, circleRepo: circleRepo, @@ -50,6 +53,7 @@ func NewHandler(cr *chRepo.ChoreRepository, circleRepo *cRepo.CircleRepository, tRepo: tRepo, lRepo: lRepo, eventProducer: ep, + stRepo: stRepo, } } @@ -259,6 +263,7 @@ func (h *Handler) createChore(c *gin.Context) { Points: choreReq.Points, CompletionWindow: choreReq.CompletionWindow, Description: choreReq.Description, + SubTasks: choreReq.SubTasks, } id, err := h.choreRepo.CreateChore(c, createdChore) createdChore.ID = id @@ -270,6 +275,10 @@ func (h *Handler) createChore(c *gin.Context) { return } + if choreReq.SubTasks != nil { + h.stRepo.CreateSubtasks(c, nil, choreReq.SubTasks, createdChore.ID) + } + var choreAssignees []*chModel.ChoreAssignees for _, assignee := range choreReq.Assignees { choreAssignees = append(choreAssignees, &chModel.ChoreAssignees{ @@ -521,6 +530,7 @@ func (h *Handler) editChore(c *gin.Context) { Points: choreReq.Points, CompletionWindow: choreReq.CompletionWindow, Description: choreReq.Description, + Priority: choreReq.Priority, } if err := h.choreRepo.UpsertChore(c, updatedChore); err != nil { c.JSON(500, gin.H{ @@ -528,6 +538,54 @@ func (h *Handler) editChore(c *gin.Context) { }) 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 { err = h.choreRepo.UpdateChoreAssignees(c, choreAssigneesToAdd) @@ -1077,6 +1135,10 @@ func (h *Handler) completeChore(c *gin.Context) { }) return } + if updatedChore.SubTasks != nil && updatedChore.FrequencyType != chModel.FrequencyTypeOnce { + h.stRepo.ResetSubtasksCompletion(c, updatedChore.ID) + } + // go func() { // h.notifier.SendChoreCompletion(c, chore, currentUser) @@ -1355,6 +1417,76 @@ func (h *Handler) DeleteHistory(c *gin.Context) { "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) { // copy the history to avoid modifying the original: 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.POST("/", h.createChore) choresRoutes.GET("/:id", h.getChore) + choresRoutes.PUT("/:id/subtask", h.UpdateSubtaskCompletedAt) choresRoutes.GET("/:id/details", h.GetChoreDetail) choresRoutes.GET("/:id/history", h.GetChoreHistory) choresRoutes.PUT("/:id/history/:history_id", h.ModifyHistory) diff --git a/internal/chore/model/model.go b/internal/chore/model/model.go index 4420000..5920e28 100644 --- a/internal/chore/model/model.go +++ b/internal/chore/model/model.go @@ -5,8 +5,8 @@ import ( cModel "donetick.com/core/internal/circle/model" lModel "donetick.com/core/internal/label/model" + stModel "donetick.com/core/internal/subtask/model" tModel "donetick.com/core/internal/thing/model" - thingModel "donetick.com/core/internal/thing/model" ) 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 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 + SubTasks *[]stModel.SubTask `json:"subTasks,omitempty" gorm:"foreignkey:ChoreID;references:ID"` // Subtasks for the chore } type Status int8 @@ -119,19 +120,20 @@ type Tag struct { } type ChoreDetail struct { - ID int `json:"id" gorm:"column:id"` - Name string `json:"name" gorm:"column:name"` - Description *string `json:"description" gorm:"column:description"` - FrequencyType string `json:"frequencyType" gorm:"column:frequency_type"` - NextDueDate *time.Time `json:"nextDueDate" gorm:"column:next_due_date"` - AssignedTo int `json:"assignedTo" gorm:"column:assigned_to"` - LastCompletedDate *time.Time `json:"lastCompletedDate" gorm:"column:last_completed_date"` - LastCompletedBy *int `json:"lastCompletedBy" gorm:"column:last_completed_by"` - TotalCompletedCount int `json:"totalCompletedCount" gorm:"column:total_completed"` - 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"` + ID int `json:"id" gorm:"column:id"` + Name string `json:"name" gorm:"column:name"` + Description *string `json:"description" gorm:"column:description"` + FrequencyType string `json:"frequencyType" gorm:"column:frequency_type"` + NextDueDate *time.Time `json:"nextDueDate" gorm:"column:next_due_date"` + AssignedTo int `json:"assignedTo" gorm:"column:assigned_to"` + LastCompletedDate *time.Time `json:"lastCompletedDate" gorm:"column:last_completed_date"` + LastCompletedBy *int `json:"lastCompletedBy" gorm:"column:last_completed_by"` + TotalCompletedCount int `json:"totalCompletedCount" gorm:"column:total_completed"` + 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"` + Subtasks *[]stModel.SubTask `json:"subTasks,omitempty" gorm:"foreignkey:ChoreID;references:ID"` } type Label struct { @@ -150,25 +152,27 @@ type ChoreLabels struct { } 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"` + 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 *tModel.ThingTrigger `json:"thingTrigger"` + Points *int `json:"points"` + CompletionWindow *int `json:"completionWindow"` + Description *string `json:"description"` + Priority int `json:"priority"` + SubTasks *[]stModel.SubTask `json:"subTasks"` } func (c *Chore) CanEdit(userID int, circleUsers []*cModel.UserCircleDetail) bool { diff --git a/internal/chore/repo/repository.go b/internal/chore/repo/repository.go index 205af9a..99ed7cb 100644 --- a/internal/chore/repo/repository.go +++ b/internal/chore/repo/repository.go @@ -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) { 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 &chore, nil @@ -279,6 +279,7 @@ func (r *ChoreRepository) GetChoreDetailByID(c context.Context, choreID int, cir var choreDetail chModel.ChoreDetail if err := r.db.WithContext(c). Table("chores"). + Preload("Subtasks"). Select(` chores.id, chores.name, diff --git a/internal/chore/scheduler.go b/internal/chore/scheduler.go index d25658b..b936278 100644 --- a/internal/chore/scheduler.go +++ b/internal/chore/scheduler.go @@ -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") 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 { 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 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) nextMonth := (currentMonth + i) % 12 // Use modulo to cycle through months if nextMonth == 0 { diff --git a/internal/chore/scheduler_test.go b/internal/chore/scheduler_test.go index 4ac737a..14c81d2 100644 --- a/internal/chore/scheduler_test.go +++ b/internal/chore/scheduler_test.go @@ -224,11 +224,25 @@ func TestScheduleNextDueDateDayOfMonth(t *testing.T) { chore: chModel.Chore{ 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(1, 1, 0), 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) diff --git a/internal/database/migration.go b/internal/database/migration.go index be0ea41..cfca864 100644 --- a/internal/database/migration.go +++ b/internal/database/migration.go @@ -10,6 +10,7 @@ import ( cModel "donetick.com/core/internal/circle/model" nModel "donetick.com/core/internal/notifier/model" pModel "donetick.com/core/internal/points" + stModel "donetick.com/core/internal/subtask/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 migrations "donetick.com/core/migrations" @@ -37,6 +38,7 @@ func Migration(db *gorm.DB) error { chModel.ChoreLabels{}, migrations.Migration{}, pModel.PointsHistory{}, + stModel.SubTask{}, ); err != nil { return err } diff --git a/internal/events/producer.go b/internal/events/producer.go index bd38f59..4928333 100644 --- a/internal/events/producer.go +++ b/internal/events/producer.go @@ -28,7 +28,8 @@ const ( // EventTypeTaskCreated EventType = "task.created" EventTypeTaskReminder EventType = "task.reminder" // EventTypeTaskUpdated EventType = "task.updated" - EventTypeTaskCompleted EventType = "task.completed" + EventTypeTaskCompleted EventType = "task.completed" + EventTypeSubTaskCompleted EventType = "subtask.completed" // EventTypeTaskReassigned EventType = "task.reassigned" EventTypeTaskSkipped EventType = "task.skipped" EventTypeThingChanged EventType = "thing.changed" @@ -172,3 +173,16 @@ func (p *EventsProducer) ThingsUpdated(ctx context.Context, url string, data int 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, + }) +} diff --git a/internal/notifier/model/model.go b/internal/notifier/model/model.go index 0eb0456..a3696df 100644 --- a/internal/notifier/model/model.go +++ b/internal/notifier/model/model.go @@ -36,6 +36,7 @@ const ( NotificationPlatformNone NotificationPlatform = iota NotificationPlatformTelegram NotificationPlatformPushover + NotificationPlatformWebhook ) type JSONB map[string]interface{} diff --git a/internal/notifier/notifier.go b/internal/notifier/notifier.go index 0bf1cc3..1ee47d4 100644 --- a/internal/notifier/notifier.go +++ b/internal/notifier/notifier.go @@ -41,6 +41,15 @@ func (n *Notifier) SendNotification(c context.Context, notification *nModel.Noti return nil } 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 { log.Error("Failed to send notification", "err", err) diff --git a/internal/subtask/model/model.go b/internal/subtask/model/model.go new file mode 100644 index 0000000..56e873d --- /dev/null +++ b/internal/subtask/model/model.go @@ -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"` +} diff --git a/internal/subtask/repo/repository.go b/internal/subtask/repo/repository.go new file mode 100644 index 0000000..9cd0e39 --- /dev/null +++ b/internal/subtask/repo/repository.go @@ -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 +} diff --git a/main.go b/main.go index da51770..bc1048b 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,7 @@ import ( label "donetick.com/core/internal/label" lRepo "donetick.com/core/internal/label/repo" "donetick.com/core/internal/resource" + spRepo "donetick.com/core/internal/subtask/repo" notifier "donetick.com/core/internal/notifier" nRepo "donetick.com/core/internal/notifier/repo" @@ -89,6 +90,7 @@ func main() { // points fx.Provide(pRepo.NewPointsRepository), + fx.Provide(spRepo.NewSubTasksRepository), // Labels: fx.Provide(lRepo.NewLabelRepository),