From e40c2a84cd93683aa7e4d98d4ada7923e32a3e33 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Tue, 2 Jul 2024 01:39:46 -0400 Subject: [PATCH 01/12] Dissociate chore with thing in deleteChore function --- internal/chore/handler.go | 2 ++ internal/thing/repo/repository.go | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/internal/chore/handler.go b/internal/chore/handler.go index 3d9f9e7..919e838 100644 --- a/internal/chore/handler.go +++ b/internal/chore/handler.go @@ -562,6 +562,8 @@ func (h *Handler) deleteChore(c *gin.Context) { return } h.nRepo.DeleteAllChoreNotifications(id) + h.tRepo.DissociateChoreWithThing(c, id) + c.JSON(200, gin.H{ "message": "Chore deleted successfully", }) diff --git a/internal/thing/repo/repository.go b/internal/thing/repo/repository.go index a7b1fc9..ad5a92f 100644 --- a/internal/thing/repo/repository.go +++ b/internal/thing/repo/repository.go @@ -70,6 +70,10 @@ func (r *ThingRepository) DissociateThingWithChore(c context.Context, thingID in return r.db.WithContext(c).Where("thing_id = ? AND chore_id = ?", thingID, choreID).Delete(&tModel.ThingChore{}).Error } +func (r *ThingRepository) DissociateChoreWithThing(c context.Context, choreID int) error { + return r.db.WithContext(c).Where("chore_id = ?", choreID).Delete(&tModel.ThingChore{}).Error +} + func (r *ThingRepository) GetThingHistoryWithOffset(c context.Context, thingID int, offset int) ([]*tModel.ThingHistory, error) { var thingHistory []*tModel.ThingHistory if err := r.db.WithContext(c).Model(&tModel.ThingHistory{}).Where("thing_id = ?", thingID).Order("created_at desc").Offset(offset).Limit(10).Find(&thingHistory).Error; err != nil { From 6845fd54f10f9f7c2b500255af578cf5078ff3f5 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Tue, 2 Jul 2024 01:40:09 -0400 Subject: [PATCH 02/12] Add validation to deleteThing endpoint to prevent deletion of things with associated tasks --- internal/notifier/service/planner.go | 3 +++ internal/thing/handler.go | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/internal/notifier/service/planner.go b/internal/notifier/service/planner.go index 22502ab..c8b4ea6 100644 --- a/internal/notifier/service/planner.go +++ b/internal/notifier/service/planner.go @@ -48,6 +48,9 @@ func (n *NotificationPlanner) GenerateNotifications(c context.Context, chore *ch var mt *chModel.NotificationMetadata if err := json.Unmarshal([]byte(*chore.NotificationMetadata), &mt); err != nil { log.Error("Error unmarshalling notification metadata", err) + return false + } + if chore.NextDueDate == nil { return true } if mt.DueDate { diff --git a/internal/thing/handler.go b/internal/thing/handler.go index 65bc871..7121c82 100644 --- a/internal/thing/handler.go +++ b/internal/thing/handler.go @@ -261,6 +261,16 @@ func (h *Handler) DeleteThing(c *gin.Context) { c.JSON(403, gin.H{"error": "Forbidden"}) return } + // confirm there are no chores associated with the thing: + thingChores, err := h.tRepo.GetThingChoresByThingId(c, thing.ID) + if err != nil { + c.JSON(500, gin.H{"error": "Unable to find tasks linked to this thing"}) + return + } + if len(thingChores) > 0 { + c.JSON(405, gin.H{"error": "Unable to delete thing with associated tasks"}) + return + } if err := h.tRepo.DeleteThing(c, thingID); err != nil { c.JSON(500, gin.H{"error": err.Error()}) return From f115d70c49048e3ec3ee768bd9cd85f9b06c1631 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Fri, 5 Jul 2024 00:34:35 -0400 Subject: [PATCH 03/12] make thing a pointer on chore --- internal/chore/handler.go | 2 +- internal/chore/model/model.go | 40 +++++++++++++++++------------------ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/internal/chore/handler.go b/internal/chore/handler.go index 919e838..d15d4e7 100644 --- a/internal/chore/handler.go +++ b/internal/chore/handler.go @@ -488,7 +488,7 @@ func (h *Handler) editChore(c *gin.Context) { go func() { h.nPlanner.GenerateNotifications(c, updatedChore) }() - if oldChore.ThingChore.ThingID != 0 { + if oldChore.ThingChore != nil { // TODO: Add check to see if dissociation is necessary h.tRepo.DissociateThingWithChore(c, oldChore.ThingChore.ThingID, oldChore.ID) diff --git a/internal/chore/model/model.go b/internal/chore/model/model.go index 4de7808..09cc658 100644 --- a/internal/chore/model/model.go +++ b/internal/chore/model/model.go @@ -7,26 +7,26 @@ import ( ) type Chore struct { - ID int `json:"id" gorm:"primary_key"` - Name string `json:"name" gorm:"column:name"` // Chore description - FrequencyType string `json:"frequencyType" gorm:"column:frequency_type"` // "daily", "weekly", "monthly", "yearly", "adaptive",or "custom" - Frequency int `json:"frequency" gorm:"column:frequency"` // Number of days, weeks, months, or years between chores - FrequencyMetadata *string `json:"frequencyMetadata" gorm:"column:frequency_meta"` // Additional frequency information - NextDueDate *time.Time `json:"nextDueDate" gorm:"column:next_due_date;index"` // When the chore is due - 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 - 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 - Labels *string `json:"labels" gorm:"column:labels"` // Labels for the chore - CircleID int `json:"circleId" gorm:"column:circle_id;index"` // The circle this chore is in - CreatedAt time.Time `json:"createdAt" gorm:"column:created_at"` // When the chore was created - UpdatedAt time.Time `json:"updatedAt" gorm:"column:updated_at"` // When the chore was last updated - 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 + ID int `json:"id" gorm:"primary_key"` + Name string `json:"name" gorm:"column:name"` // Chore description + FrequencyType string `json:"frequencyType" gorm:"column:frequency_type"` // "daily", "weekly", "monthly", "yearly", "adaptive",or "custom" + Frequency int `json:"frequency" gorm:"column:frequency"` // Number of days, weeks, months, or years between chores + FrequencyMetadata *string `json:"frequencyMetadata" gorm:"column:frequency_meta"` // Additional frequency information + NextDueDate *time.Time `json:"nextDueDate" gorm:"column:next_due_date;index"` // When the chore is due + 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 + 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 + Labels *string `json:"labels" gorm:"column:labels"` // Labels for the chore + CircleID int `json:"circleId" gorm:"column:circle_id;index"` // The circle this chore is in + CreatedAt time.Time `json:"createdAt" gorm:"column:created_at"` // When the chore was created + UpdatedAt time.Time `json:"updatedAt" gorm:"column:updated_at"` // When the chore was last updated + 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 } type ChoreAssignees struct { ID int `json:"-" gorm:"primary_key"` From 45e18c8edd55e98712d5f175ecaaae86fde4d933 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sat, 6 Jul 2024 02:36:14 -0400 Subject: [PATCH 04/12] Add GetChoreDetail endpoint to retrieve detailed chore information --- internal/chore/handler.go | 32 +++++++++++++++++++++++++++ internal/chore/model/model.go | 12 +++++++++++ internal/chore/repo/repository.go | 36 +++++++++++++++++++++++++++++++ internal/user/handler.go | 2 ++ 4 files changed, 82 insertions(+) diff --git a/internal/chore/handler.go b/internal/chore/handler.go index d15d4e7..bce6d01 100644 --- a/internal/chore/handler.go +++ b/internal/chore/handler.go @@ -873,6 +873,37 @@ func (h *Handler) GetChoreHistory(c *gin.Context) { }) } +func (h *Handler) GetChoreDetail(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") + id, err := strconv.Atoi(rawID) + if err != nil { + c.JSON(400, gin.H{ + "error": "Invalid ID", + }) + return + } + + detailed, err := h.choreRepo.GetChoreDetailByID(c, id, currentUser.CircleID) + if err != nil { + c.JSON(500, gin.H{ + "error": "Error getting chore history", + }) + return + } + + c.JSON(200, gin.H{ + "res": detailed, + }) +} + 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)) @@ -959,6 +990,7 @@ func Routes(router *gin.Engine, h *Handler, auth *jwt.GinJWTMiddleware) { choresRoutes.PUT("/", h.editChore) choresRoutes.POST("/", h.createChore) choresRoutes.GET("/:id", h.getChore) + choresRoutes.GET("/:id/details", h.GetChoreDetail) choresRoutes.GET("/:id/history", h.GetChoreHistory) choresRoutes.POST("/:id/do", h.completeChore) choresRoutes.POST("/:id/skip", h.skipChore) diff --git a/internal/chore/model/model.go b/internal/chore/model/model.go index 09cc658..2e7ee90 100644 --- a/internal/chore/model/model.go +++ b/internal/chore/model/model.go @@ -70,3 +70,15 @@ type Tag struct { // CircleID int `json:"circleId" gorm:"primaryKey;autoIncrement:false"` // TagID int `json:"tagId" gorm:"primaryKey;autoIncrement:false"` // } + +type ChoreDetail struct { + ID int `json:"id" gorm:"column:id"` + Name string `json:"name" gorm:"column:name"` + 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"` + CreatedBy int `json:"createdBy" gorm:"column:created_by"` +} diff --git a/internal/chore/repo/repository.go b/internal/chore/repo/repository.go index 1ab0f0b..52fa4ab 100644 --- a/internal/chore/repo/repository.go +++ b/internal/chore/repo/repository.go @@ -214,3 +214,39 @@ func (r *ChoreRepository) SetDueDate(c context.Context, choreID int, dueDate tim func (r *ChoreRepository) SetDueDateIfNotExisted(c context.Context, choreID int, dueDate time.Time) error { return r.db.WithContext(c).Model(&chModel.Chore{}).Where("id = ? and next_due_date is null", choreID).Update("next_due_date", dueDate).Error } + +func (r *ChoreRepository) GetChoreDetailByID(c context.Context, choreID int, circleID int) (*chModel.ChoreDetail, error) { + var choreDetail chModel.ChoreDetail + if err := r.db.WithContext(c). + Table("chores"). + Select(` + chores.id, + chores.name, + chores.frequency_type, + chores.next_due_date, + chores.assigned_to, + chores.created_by, + recent_history.last_completed_date, + recent_history.last_assigned_to as last_completed_by, + COUNT(chore_histories.id) as total_completed`). + Joins("LEFT JOIN chore_histories ON chores.id = chore_histories.chore_id"). + Joins(`LEFT JOIN ( + SELECT + chore_id, + assigned_to AS last_assigned_to, + completed_at AS last_completed_date + FROM chore_histories + WHERE (chore_id, completed_at) IN ( + SELECT chore_id, MAX(completed_at) + FROM chore_histories + GROUP BY chore_id + ) + ) AS recent_history ON chores.id = recent_history.chore_id`). + Where("chores.id = ? and chores.circle_id = ?", choreID, circleID). + Group("chores.id"). + First(&choreDetail).Error; err != nil { + return nil, err + + } + return &choreDetail, nil +} diff --git a/internal/user/handler.go b/internal/user/handler.go index 0eee6f2..961851f 100644 --- a/internal/user/handler.go +++ b/internal/user/handler.go @@ -69,6 +69,7 @@ func (h *Handler) signUp(c *gin.Context) { type SignUpReq struct { Username string `json:"username" binding:"required,min=4,max=20"` Password string `json:"password" binding:"required,min=8,max=45"` + Email string `json:"email" binding:"required,email"` DisplayName string `json:"displayName"` } var signupReq SignUpReq @@ -96,6 +97,7 @@ func (h *Handler) signUp(c *gin.Context) { Username: signupReq.Username, Password: password, DisplayName: signupReq.DisplayName, + Email: signupReq.Email, CreatedAt: time.Now(), UpdatedAt: time.Now(), }); err != nil { From 36f4717454f9da8dff013d0387882c6633662faa Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sat, 6 Jul 2024 03:15:56 -0400 Subject: [PATCH 05/12] Group chore detail query by chore ID and last completed date --- internal/chore/repo/repository.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/chore/repo/repository.go b/internal/chore/repo/repository.go index 52fa4ab..51ebb2c 100644 --- a/internal/chore/repo/repository.go +++ b/internal/chore/repo/repository.go @@ -243,7 +243,7 @@ func (r *ChoreRepository) GetChoreDetailByID(c context.Context, choreID int, cir ) ) AS recent_history ON chores.id = recent_history.chore_id`). Where("chores.id = ? and chores.circle_id = ?", choreID, circleID). - Group("chores.id"). + Group("chores.id, recent_history.last_completed_date"). First(&choreDetail).Error; err != nil { return nil, err From c92cdb427d050456769f520cdcc4ee10db895ad9 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sat, 6 Jul 2024 03:26:11 -0400 Subject: [PATCH 06/12] Add GetChoreHistoryWithLimit method to ChoreRepository --- internal/chore/repo/repository.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/chore/repo/repository.go b/internal/chore/repo/repository.go index 51ebb2c..4eac59c 100644 --- a/internal/chore/repo/repository.go +++ b/internal/chore/repo/repository.go @@ -111,6 +111,13 @@ func (r *ChoreRepository) GetChoreHistory(c context.Context, choreID int) ([]*ch } return histories, nil } +func (r *ChoreRepository) GetChoreHistoryWithLimit(c context.Context, choreID int, limit int) ([]*chModel.ChoreHistory, error) { + var histories []*chModel.ChoreHistory + if err := r.db.WithContext(c).Where("chore_id = ?", choreID).Order("completed_at desc").Limit(limit).Find(&histories).Error; err != nil { + return nil, err + } + return histories, nil +} func (r *ChoreRepository) UpdateChoreAssignees(c context.Context, assignees []*chModel.ChoreAssignees) error { return r.db.WithContext(c).Save(&assignees).Error @@ -243,7 +250,7 @@ func (r *ChoreRepository) GetChoreDetailByID(c context.Context, choreID int, cir ) ) AS recent_history ON chores.id = recent_history.chore_id`). Where("chores.id = ? and chores.circle_id = ?", choreID, circleID). - Group("chores.id, recent_history.last_completed_date"). + Group("chores.id, recent_history.last_completed_date, recent_history.last_assigned_to"). First(&choreDetail).Error; err != nil { return nil, err From 010db330a46ad32daf30400283971cd4e9835e1d Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sun, 7 Jul 2024 00:26:08 -0400 Subject: [PATCH 07/12] chore: Update chore detail query to include notes in result --- internal/chore/handler.go | 33 ++++++++++++++++++++++++------- internal/chore/model/model.go | 1 + internal/chore/repo/repository.go | 7 +++++-- internal/chore/scheduler.go | 28 ++++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 9 deletions(-) diff --git a/internal/chore/handler.go b/internal/chore/handler.go index bce6d01..5ec8cdd 100644 --- a/internal/chore/handler.go +++ b/internal/chore/handler.go @@ -802,14 +802,33 @@ func (h *Handler) completeChore(c *gin.Context) { }) return } + var nextDueDate *time.Time + if chore.FrequencyType == "adaptive" { + history, err := h.choreRepo.GetChoreHistoryWithLimit(c, chore.ID, 5) + if err != nil { + c.JSON(500, gin.H{ + "error": "Error getting chore history", + }) + return + } + nextDueDate, err = scheduleAdaptiveNextDueDate(chore, completedDate, history) + if err != nil { + log.Printf("Error scheduling next due date: %s", err) + c.JSON(500, gin.H{ + "error": "Error scheduling next due date", + }) + return + } - nextDueDate, err := scheduleNextDueDate(chore, completedDate) - if err != nil { - log.Printf("Error scheduling next due date: %s", err) - c.JSON(500, gin.H{ - "error": "Error scheduling next due date", - }) - return + } else { + nextDueDate, err = scheduleNextDueDate(chore, completedDate) + if err != nil { + log.Printf("Error scheduling next due date: %s", err) + c.JSON(500, gin.H{ + "error": "Error scheduling next due date", + }) + return + } } choreHistory, err := h.choreRepo.GetChoreHistory(c, chore.ID) if err != nil { diff --git a/internal/chore/model/model.go b/internal/chore/model/model.go index 2e7ee90..90f6c29 100644 --- a/internal/chore/model/model.go +++ b/internal/chore/model/model.go @@ -80,5 +80,6 @@ type ChoreDetail struct { 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"` + Notes *string `json:"notes" gorm:"column:notes"` CreatedBy int `json:"createdBy" gorm:"column:created_by"` } diff --git a/internal/chore/repo/repository.go b/internal/chore/repo/repository.go index 4eac59c..7284202 100644 --- a/internal/chore/repo/repository.go +++ b/internal/chore/repo/repository.go @@ -234,6 +234,7 @@ func (r *ChoreRepository) GetChoreDetailByID(c context.Context, choreID int, cir chores.assigned_to, chores.created_by, recent_history.last_completed_date, + recent_history.notes, recent_history.last_assigned_to as last_completed_by, COUNT(chore_histories.id) as total_completed`). Joins("LEFT JOIN chore_histories ON chores.id = chore_histories.chore_id"). @@ -241,7 +242,9 @@ func (r *ChoreRepository) GetChoreDetailByID(c context.Context, choreID int, cir SELECT chore_id, assigned_to AS last_assigned_to, - completed_at AS last_completed_date + completed_at AS last_completed_date, + notes + FROM chore_histories WHERE (chore_id, completed_at) IN ( SELECT chore_id, MAX(completed_at) @@ -250,7 +253,7 @@ func (r *ChoreRepository) GetChoreDetailByID(c context.Context, choreID int, cir ) ) AS recent_history ON chores.id = recent_history.chore_id`). Where("chores.id = ? and chores.circle_id = ?", choreID, circleID). - Group("chores.id, recent_history.last_completed_date, recent_history.last_assigned_to"). + Group("chores.id, recent_history.last_completed_date, recent_history.last_assigned_to, recent_history.notes"). First(&choreDetail).Error; err != nil { return nil, err diff --git a/internal/chore/scheduler.go b/internal/chore/scheduler.go index 55cdf01..c1ff48d 100644 --- a/internal/chore/scheduler.go +++ b/internal/chore/scheduler.go @@ -47,6 +47,7 @@ func scheduleNextDueDate(chore *chModel.Chore, completedDate time.Time) (*time.T } else if chore.FrequencyType == "yearly" { nextDueDate = baseDate.AddDate(1, 0, 0) } else if chore.FrequencyType == "adaptive" { + // TODO: calculate next due date based on the history of the chore // calculate the difference between the due date and now in days: diff := completedDate.UTC().Sub(chore.NextDueDate.UTC()) @@ -129,6 +130,33 @@ func scheduleNextDueDate(chore *chModel.Chore, completedDate time.Time) (*time.T } +func scheduleAdaptiveNextDueDate(chore *chModel.Chore, completedDate time.Time, history []*chModel.ChoreHistory) (*time.Time, error) { + // will generate due date base on history and the different between the completed date and the due date + // the more recent the higher weight + if len(history) <= 1 { + if chore.NextDueDate != nil { + diff := completedDate.UTC().Sub(chore.NextDueDate.UTC()) + nextDueDate := completedDate.UTC().Add(diff) + return &nextDueDate, nil + } + return nil, nil + } + var weight float64 + var totalWeight float64 + var nextDueDate time.Time + for i := 0; i < len(history)-1; i++ { + delay := history[i].CompletedAt.UTC().Sub(history[i+1].CompletedAt.UTC()).Seconds() + weight = delay * float64(len(history)-i) + totalWeight += weight + } + // calculate the average delay + averageDelay := totalWeight / float64(len(history)-1) + // calculate the difference between the completed date and the due date + nextDueDate = completedDate.UTC().Add(time.Duration(averageDelay) * time.Second) + + return &nextDueDate, nil +} + func RemoveAssigneeAndReassign(chore *chModel.Chore, userID int) { for i, assignee := range chore.Assignees { if assignee.UserID == userID { From 2004031055b58c8152ff0a5441ee7e5337c1acbc Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sun, 7 Jul 2024 00:31:19 -0400 Subject: [PATCH 08/12] Update chore notification messages to use markdown formatting --- internal/notifier/service/planner.go | 4 ++-- internal/notifier/telegram/telegram.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/notifier/service/planner.go b/internal/notifier/service/planner.go index c8b4ea6..615fa0d 100644 --- a/internal/notifier/service/planner.go +++ b/internal/notifier/service/planner.go @@ -86,7 +86,7 @@ func generateDueNotifications(chore *chModel.Chore, users []*cModel.UserCircleDe TypeID: 1, UserID: user.ID, TargetID: fmt.Sprint(user.ChatID), - Text: fmt.Sprintf("📅 Reminder: '%s' is due today and assigned to %s.", chore.Name, assignee.DisplayName), + Text: fmt.Sprintf("📅 Reminder: *%s* is due today and assigned to %s.", chore.Name, assignee.DisplayName), } notifications = append(notifications, notification) } @@ -141,7 +141,7 @@ func generateOverdueNotifications(chore *chModel.Chore, users []*cModel.UserCirc TypeID: 2, UserID: user.ID, TargetID: fmt.Sprint(user.ChatID), - Text: fmt.Sprintf("🚨 '%s' is now %d hours overdue. Please complete it as soon as possible. (Assigned to %s)", chore.Name, hours, assignee.DisplayName), + Text: fmt.Sprintf("🚨 *%s* is now %d hours overdue. Please complete it as soon as possible. (Assigned to %s)", chore.Name, hours, assignee.DisplayName), } notifications = append(notifications, notification) } diff --git a/internal/notifier/telegram/telegram.go b/internal/notifier/telegram/telegram.go index e35f0c8..54c0905 100644 --- a/internal/notifier/telegram/telegram.go +++ b/internal/notifier/telegram/telegram.go @@ -55,7 +55,7 @@ func (tn *TelegramNotifier) SendChoreCompletion(c context.Context, chore *chMode if user.ChatID == 0 { continue } - text := fmt.Sprintf("🎉 '%s' is completed! is off the list, %s! 🌟 ", chore.Name, user.DisplayName) + text := fmt.Sprintf("🎉 *%s* is completed! is off the list, %s! 🌟 ", chore.Name, user.DisplayName) msg := tgbotapi.NewMessage(user.ChatID, text) msg.ParseMode = "Markdown" _, err := tn.bot.Send(msg) From 9fe382d26e5d8cbe707468bb9a019067fec7d291 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sun, 7 Jul 2024 02:52:52 -0400 Subject: [PATCH 09/12] Update chore notification messages to use markdown formatting --- internal/notifier/service/planner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/notifier/service/planner.go b/internal/notifier/service/planner.go index 615fa0d..05d600d 100644 --- a/internal/notifier/service/planner.go +++ b/internal/notifier/service/planner.go @@ -112,7 +112,7 @@ func generatePreDueNotifications(chore *chModel.Chore, users []*cModel.UserCircl TypeID: 3, UserID: user.ID, TargetID: fmt.Sprint(user.ChatID), - Text: fmt.Sprintf("📢 Heads up! Chore '%s' is due soon (on %s) and assigned to %s.", chore.Name, chore.NextDueDate.Format("January 2nd"), assignee.DisplayName), + Text: fmt.Sprintf("📢 Heads up! *%s* is due soon (on %s) and assigned to %s.", chore.Name, chore.NextDueDate.Format("January 2nd"), assignee.DisplayName), } notifications = append(notifications, notification) From 970cf40745a7441462461467274dc6b1b3de5eb8 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sun, 7 Jul 2024 02:59:04 -0400 Subject: [PATCH 10/12] resetPassword function to handle user not found case --- internal/user/handler.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/user/handler.go b/internal/user/handler.go index 961851f..15e881f 100644 --- a/internal/user/handler.go +++ b/internal/user/handler.go @@ -283,9 +283,8 @@ func (h *Handler) resetPassword(c *gin.Context) { } user, err := h.userRepo.FindByEmail(c, req.Email) if err != nil { - c.JSON(http.StatusNotFound, gin.H{ - "error": "User not found", - }) + c.JSON(http.StatusOK, gin.H{}) + log.Error("account.handler.resetPassword failed to find user") return } if user.Provider != 0 { From e862a281d990fce370a56f44fe0da9be435da90c Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sun, 7 Jul 2024 19:53:08 -0400 Subject: [PATCH 11/12] Add time field to FrequencyMetadata struct --- internal/chore/model/model.go | 1 + internal/chore/scheduler.go | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/internal/chore/model/model.go b/internal/chore/model/model.go index 90f6c29..3fb7a92 100644 --- a/internal/chore/model/model.go +++ b/internal/chore/model/model.go @@ -47,6 +47,7 @@ type FrequencyMetadata struct { Days []*string `json:"days,omitempty"` Months []*string `json:"months,omitempty"` Unit *string `json:"unit,omitempty"` + Time string `json:"time,omitempty"` } type NotificationMetadata struct { diff --git a/internal/chore/scheduler.go b/internal/chore/scheduler.go index c1ff48d..5413447 100644 --- a/internal/chore/scheduler.go +++ b/internal/chore/scheduler.go @@ -22,6 +22,7 @@ func scheduleNextDueDate(chore *chModel.Chore, completedDate time.Time) (*time.T if chore.FrequencyType == "once" { return nil, nil } + if chore.NextDueDate != nil { // no due date set, use the current date @@ -29,6 +30,17 @@ func scheduleNextDueDate(chore *chModel.Chore, completedDate time.Time) (*time.T } else { baseDate = completedDate.UTC() } + if chore.FrequencyType == "day_of_the_month" || chore.FrequencyType == "days_of_the_week" || chore.FrequencyType == "interval" { + // time in frequency metadata stored as RFC3339 format like `2024-07-07T13:27:00-04:00` + // parse it to time.Time: + t, err := time.Parse(time.RFC3339, frequencyMetadata.Time) + if err != nil { + return nil, fmt.Errorf("error parsing time in frequency metadata") + } + // set the time to the time in the frequency metadata: + baseDate = time.Date(baseDate.Year(), baseDate.Month(), baseDate.Day(), t.Hour(), t.Minute(), 0, 0, t.Location()) + + } if chore.IsRolling && chore.NextDueDate.Before(completedDate) { // we need to check if chore due date is before the completed date to handle this senario: // if user trying to complete chore due in future (multiple time for insance) due date will be calculated From 2d538ff43c9b73223e4af8f8668d0f4f1412e3ab Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Tue, 9 Jul 2024 18:31:10 -0400 Subject: [PATCH 12/12] Update database configuration to support custom SQLite path --- internal/database/database.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/database/database.go b/internal/database/database.go index 8fc8a68..67818db 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -2,6 +2,7 @@ package database import ( "fmt" + "os" "time" "gorm.io/driver/postgres" @@ -33,7 +34,12 @@ func NewDatabase(cfg *config.Config) (*gorm.DB, error) { default: - db, err = gorm.Open(sqlite.Open("donetick.db"), &gorm.Config{}) + path := os.Getenv("DT_SQLITE_PATH") + if path == "" { + db, err = gorm.Open(sqlite.Open("donetick.db"), &gorm.Config{}) + } else { + db, err = gorm.Open(sqlite.Open(path), &gorm.Config{}) + } }