aboutsummaryrefslogtreecommitdiffstats
path: root/internal/chore/handler.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/chore/handler.go')
-rw-r--r--internal/chore/handler.go974
1 files changed, 974 insertions, 0 deletions
diff --git a/internal/chore/handler.go b/internal/chore/handler.go
new file mode 100644
index 0000000..bc90c4c
--- /dev/null
+++ b/internal/chore/handler.go
@@ -0,0 +1,974 @@
+package chore
+
+import (
+ "encoding/json"
+ "fmt"
+ "html"
+ "log"
+ "math"
+ "math/rand"
+ "strconv"
+ "strings"
+ "time"
+
+ auth "donetick.com/core/internal/authorization"
+ chModel "donetick.com/core/internal/chore/model"
+ chRepo "donetick.com/core/internal/chore/repo"
+ cRepo "donetick.com/core/internal/circle/repo"
+ nRepo "donetick.com/core/internal/notifier/repo"
+ nps "donetick.com/core/internal/notifier/service"
+ telegram "donetick.com/core/internal/notifier/telegram"
+ tRepo "donetick.com/core/internal/thing/repo"
+ uModel "donetick.com/core/internal/user/model"
+ "donetick.com/core/logging"
+ jwt "github.com/appleboy/gin-jwt/v2"
+ "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 ChoreReq struct {
+ Name string `json:"name" binding:"required"`
+ FrequencyType string `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"`
+ ThingTrigger *ThingTrigger `json:"thingTrigger"`
+}
+type Handler struct {
+ choreRepo *chRepo.ChoreRepository
+ circleRepo *cRepo.CircleRepository
+ notifier *telegram.TelegramNotifier
+ nPlanner *nps.NotificationPlanner
+ nRepo *nRepo.NotificationRepository
+ tRepo *tRepo.ThingRepository
+}
+
+func NewHandler(cr *chRepo.ChoreRepository, circleRepo *cRepo.CircleRepository, nt *telegram.TelegramNotifier,
+ np *nps.NotificationPlanner, nRepo *nRepo.NotificationRepository, tRepo *tRepo.ThingRepository) *Handler {
+ return &Handler{
+ choreRepo: cr,
+ circleRepo: circleRepo,
+ notifier: nt,
+ nPlanner: np,
+ nRepo: nRepo,
+ tRepo: tRepo,
+ }
+}
+
+func (h *Handler) getChores(c *gin.Context) {
+ u, ok := auth.CurrentUser(c)
+ if !ok {
+ c.JSON(500, gin.H{
+ "error": "Error getting current circle",
+ })
+ return
+ }
+ chores, err := h.choreRepo.GetChores(c, u.CircleID, u.ID)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error getting chores",
+ })
+ return
+ }
+
+ c.JSON(200, gin.H{
+ "res": chores,
+ })
+}
+
+func (h *Handler) getChore(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
+ }
+
+ chore, err := h.choreRepo.GetChore(c, id)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error getting chore",
+ })
+ return
+ }
+ isAssignee := false
+
+ for _, assignee := range chore.Assignees {
+ if assignee.UserID == currentUser.ID {
+ isAssignee = true
+ break
+ }
+ }
+
+ if currentUser.ID != chore.CreatedBy && !isAssignee {
+ c.JSON(403, gin.H{
+ "error": "You are not allowed to view this chore",
+ })
+ return
+ }
+
+ c.JSON(200, gin.H{
+ "res": chore,
+ })
+}
+
+func (h *Handler) createChore(c *gin.Context) {
+ logger := logging.FromContext(c)
+ currentUser, ok := auth.CurrentUser(c)
+
+ logger.Debug("Create chore", "currentUser", currentUser)
+ if !ok {
+ c.JSON(500, gin.H{
+ "error": "Error getting current user",
+ })
+ return
+ }
+ // Validate chore:
+ var choreReq ChoreReq
+ if err := c.ShouldBindJSON(&choreReq); err != nil {
+ log.Print(err)
+ c.JSON(400, gin.H{
+ "error": "Invalid request",
+ })
+ return
+ }
+
+ circleUsers, err := h.circleRepo.GetCircleUsers(c, currentUser.CircleID)
+ for _, assignee := range choreReq.Assignees {
+ userFound := false
+ for _, circleUser := range circleUsers {
+ if assignee.UserID == circleUser.UserID {
+ userFound = true
+
+ break
+
+ }
+ }
+ if !userFound {
+ c.JSON(400, gin.H{
+ "error": "Assignee not found in circle",
+ })
+ return
+ }
+
+ }
+ if choreReq.AssignedTo <= 0 && len(choreReq.Assignees) > 0 {
+ // if the assigned to field is not set, randomly assign the chore to one of the assignees
+ choreReq.AssignedTo = choreReq.Assignees[rand.Intn(len(choreReq.Assignees))].UserID
+ }
+
+ var dueDate *time.Time
+
+ if choreReq.DueDate != "" {
+ rawDueDate, err := time.Parse(time.RFC3339, choreReq.DueDate)
+ rawDueDate = rawDueDate.UTC()
+ dueDate = &rawDueDate
+ if err != nil {
+ c.JSON(400, gin.H{
+ "error": "Invalid date",
+ })
+ return
+ }
+
+ } else {
+ c.JSON(400, gin.H{
+ "error": "Due date is required",
+ })
+ return
+
+ }
+
+ freqencyMetadataBytes, err := json.Marshal(choreReq.FrequencyMetadata)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error marshalling frequency metadata",
+ })
+ return
+ }
+ stringFrequencyMetadata := string(freqencyMetadataBytes)
+
+ notificationMetadataBytes, err := json.Marshal(choreReq.NotificationMetadata)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error marshalling notification metadata",
+ })
+ return
+ }
+ stringNotificationMetadata := string(notificationMetadataBytes)
+
+ var stringLabels *string
+ if len(choreReq.Labels) > 0 {
+ var escapedLabels []string
+ for _, label := range choreReq.Labels {
+ escapedLabels = append(escapedLabels, html.EscapeString(label))
+ }
+
+ labels := strings.Join(escapedLabels, ",")
+ stringLabels = &labels
+ }
+ createdChore := &chModel.Chore{
+
+ Name: choreReq.Name,
+ FrequencyType: choreReq.FrequencyType,
+ Frequency: choreReq.Frequency,
+ FrequencyMetadata: &stringFrequencyMetadata,
+ NextDueDate: dueDate,
+ AssignStrategy: choreReq.AssignStrategy,
+ AssignedTo: choreReq.AssignedTo,
+ IsRolling: choreReq.IsRolling,
+ UpdatedBy: currentUser.ID,
+ IsActive: true,
+ Notification: choreReq.Notification,
+ NotificationMetadata: &stringNotificationMetadata,
+ Labels: stringLabels,
+ CreatedBy: currentUser.ID,
+ CreatedAt: time.Now().UTC(),
+ CircleID: currentUser.CircleID,
+ }
+ id, err := h.choreRepo.CreateChore(c, createdChore)
+ createdChore.ID = id
+
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error creating chore",
+ })
+ return
+ }
+
+ var choreAssignees []*chModel.ChoreAssignees
+ for _, assignee := range choreReq.Assignees {
+ choreAssignees = append(choreAssignees, &chModel.ChoreAssignees{
+ ChoreID: id,
+ UserID: assignee.UserID,
+ })
+ }
+
+ if err := h.choreRepo.UpdateChoreAssignees(c, choreAssignees); err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error adding chore assignees",
+ })
+ return
+ }
+ go func() {
+ h.nPlanner.GenerateNotifications(c, createdChore)
+ }()
+ shouldReturn := HandleThingAssociation(choreReq, h, c, currentUser)
+ if shouldReturn {
+ return
+ }
+ c.JSON(200, gin.H{
+ "res": id,
+ })
+}
+
+func (h *Handler) editChore(c *gin.Context) {
+ // logger := logging.FromContext(c)
+ currentUser, ok := auth.CurrentUser(c)
+ if !ok {
+ c.JSON(500, gin.H{
+ "error": "Error getting current user",
+ })
+ return
+ }
+
+ var choreReq ChoreReq
+ if err := c.ShouldBindJSON(&choreReq); err != nil {
+ log.Print(err)
+ c.JSON(400, gin.H{
+ "error": "Invalid request",
+ })
+ return
+ }
+
+ circleUsers, err := h.circleRepo.GetCircleUsers(c, currentUser.CircleID)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error getting circle users",
+ })
+ return
+ }
+
+ existedChoreAssignees, err := h.choreRepo.GetChoreAssignees(c, choreReq.ID)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error getting chore assignees",
+ })
+ return
+ }
+
+ var choreAssigneesToAdd []*chModel.ChoreAssignees
+ var choreAssigneesToDelete []*chModel.ChoreAssignees
+
+ // filter assignees that not in the circle
+ for _, assignee := range choreReq.Assignees {
+ userFound := false
+ for _, circleUser := range circleUsers {
+ if assignee.UserID == circleUser.UserID {
+ userFound = true
+ break
+ }
+ }
+ if !userFound {
+ c.JSON(400, gin.H{
+ "error": "Assignee not found in circle",
+ })
+ return
+ }
+ userAlreadyAssignee := false
+ for _, existedChoreAssignee := range existedChoreAssignees {
+ if existedChoreAssignee.UserID == assignee.UserID {
+ userAlreadyAssignee = true
+ break
+ }
+ }
+ if !userAlreadyAssignee {
+ choreAssigneesToAdd = append(choreAssigneesToAdd, &chModel.ChoreAssignees{
+ ChoreID: choreReq.ID,
+ UserID: assignee.UserID,
+ })
+ }
+ }
+
+ // remove assignees if they are not in the assignees list anymore
+ for _, existedChoreAssignee := range existedChoreAssignees {
+ userFound := false
+ for _, assignee := range choreReq.Assignees {
+ if existedChoreAssignee.UserID == assignee.UserID {
+ userFound = true
+ break
+ }
+ }
+ if !userFound {
+ choreAssigneesToDelete = append(choreAssigneesToDelete, existedChoreAssignee)
+ }
+ }
+
+ var dueDate *time.Time
+
+ if choreReq.DueDate != "" {
+ rawDueDate, err := time.Parse(time.RFC3339, choreReq.DueDate)
+ rawDueDate = rawDueDate.UTC()
+ dueDate = &rawDueDate
+ if err != nil {
+ c.JSON(400, gin.H{
+ "error": "Invalid date",
+ })
+ return
+ }
+
+ }
+
+ // validate assignedTo part of the assignees:
+ assigneeFound := false
+ for _, assignee := range choreReq.Assignees {
+ if assignee.UserID == choreReq.AssignedTo {
+ assigneeFound = true
+ break
+ }
+ }
+ if !assigneeFound {
+ c.JSON(400, gin.H{
+ "error": "Assigned to not found in assignees",
+ })
+ return
+ }
+
+ if choreReq.AssignedTo <= 0 && len(choreReq.Assignees) > 0 {
+ // if the assigned to field is not set, randomly assign the chore to one of the assignees
+ choreReq.AssignedTo = choreReq.Assignees[rand.Intn(len(choreReq.Assignees))].UserID
+ }
+ oldChore, err := h.choreRepo.GetChore(c, choreReq.ID)
+
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error getting chore",
+ })
+ return
+ }
+ if currentUser.ID != oldChore.CreatedBy {
+ c.JSON(403, gin.H{
+ "error": "You are not allowed to edit this chore",
+ })
+ return
+ }
+ freqencyMetadataBytes, err := json.Marshal(choreReq.FrequencyMetadata)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error marshalling frequency metadata",
+ })
+ return
+ }
+
+ stringFrequencyMetadata := string(freqencyMetadataBytes)
+
+ notificationMetadataBytes, err := json.Marshal(choreReq.NotificationMetadata)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error marshalling notification metadata",
+ })
+ return
+ }
+ stringNotificationMetadata := string(notificationMetadataBytes)
+
+ // escape special characters in labels and store them as a string :
+ var stringLabels *string
+ if len(choreReq.Labels) > 0 {
+ var escapedLabels []string
+ for _, label := range choreReq.Labels {
+ escapedLabels = append(escapedLabels, html.EscapeString(label))
+ }
+
+ labels := strings.Join(escapedLabels, ",")
+ stringLabels = &labels
+ }
+ updatedChore := &chModel.Chore{
+ ID: choreReq.ID,
+ Name: choreReq.Name,
+ FrequencyType: choreReq.FrequencyType,
+ Frequency: choreReq.Frequency,
+ FrequencyMetadata: &stringFrequencyMetadata,
+ // Assignees: &assignees,
+ NextDueDate: dueDate,
+ AssignStrategy: choreReq.AssignStrategy,
+ AssignedTo: choreReq.AssignedTo,
+ IsRolling: choreReq.IsRolling,
+ IsActive: choreReq.IsActive,
+ Notification: choreReq.Notification,
+ NotificationMetadata: &stringNotificationMetadata,
+ Labels: stringLabels,
+ CircleID: oldChore.CircleID,
+ UpdatedBy: currentUser.ID,
+ CreatedBy: oldChore.CreatedBy,
+ CreatedAt: oldChore.CreatedAt,
+ }
+ if err := h.choreRepo.UpsertChore(c, updatedChore); err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error adding chore",
+ })
+ return
+ }
+ if len(choreAssigneesToAdd) > 0 {
+ err = h.choreRepo.UpdateChoreAssignees(c, choreAssigneesToAdd)
+
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error updating chore assignees",
+ })
+ return
+ }
+ }
+ if len(choreAssigneesToDelete) > 0 {
+ err = h.choreRepo.DeleteChoreAssignees(c, choreAssigneesToDelete)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error deleting chore assignees",
+ })
+ return
+ }
+ }
+ go func() {
+ h.nPlanner.GenerateNotifications(c, updatedChore)
+ }()
+ if oldChore.ThingChore.ThingID != 0 {
+ // TODO: Add check to see if dissociation is necessary
+ h.tRepo.DissociateThingWithChore(c, oldChore.ThingChore.ThingID, oldChore.ID)
+
+ }
+ shouldReturn := HandleThingAssociation(choreReq, h, c, currentUser)
+ if shouldReturn {
+ return
+ }
+
+ c.JSON(200, gin.H{
+ "message": "Chore added successfully",
+ })
+}
+
+func HandleThingAssociation(choreReq 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 {
+ c.JSON(500, gin.H{
+ "error": "Error getting thing",
+ })
+ return true
+ }
+ if thing.UserID != currentUser.ID {
+ c.JSON(403, gin.H{
+ "error": "You are not allowed to trigger this thing",
+ })
+ return true
+ }
+ if err := h.tRepo.AssociateThingWithChore(c, choreReq.ThingTrigger.ID, choreReq.ID, choreReq.ThingTrigger.TriggerState, choreReq.ThingTrigger.Condition); err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error associating thing with chore",
+ })
+ return true
+ }
+
+ }
+ return false
+}
+
+func (h *Handler) deleteChore(c *gin.Context) {
+ // logger := logging.FromContext(c)
+ 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
+ }
+ // check if the user is the owner of the chore before deleting
+ if err := h.choreRepo.IsChoreOwner(c, id, currentUser.ID); err != nil {
+ c.JSON(403, gin.H{
+ "error": "You are not allowed to delete this chore",
+ })
+ return
+ }
+
+ if err := h.choreRepo.DeleteChore(c, id); err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error deleting chore",
+ })
+ return
+ }
+ h.nRepo.DeleteAllChoreNotifications(id)
+ c.JSON(200, gin.H{
+ "message": "Chore deleted successfully",
+ })
+}
+
+// func (h *Handler) createChore(c *gin.Context) {
+// logger := logging.FromContext(c)
+// currentUser, ok := auth.CurrentUser(c)
+
+// logger.Debug("Create chore", "currentUser", currentUser)
+// if !ok {
+// c.JSON(500, gin.H{
+// "error": "Error getting current user",
+// })
+// return
+// }
+// id, err := h.choreRepo.CreateChore(currentUser.ID, currentUser.CircleID)
+// if err != nil {
+// c.JSON(500, gin.H{
+// "error": "Error creating chore",
+// })
+// return
+// }
+
+// c.JSON(200, gin.H{
+// "res": id,
+// })
+// }
+
+func (h *Handler) updateAssignee(c *gin.Context) {
+ rawID := c.Param("id")
+ id, err := strconv.Atoi(rawID)
+ if err != nil {
+ c.JSON(400, gin.H{
+ "error": "Invalid ID",
+ })
+ return
+ }
+ type AssigneeReq struct {
+ AssignedTo int `json:"assignedTo" binding:"required"`
+ UpdatedBy int `json:"updatedBy" binding:"required"`
+ }
+
+ var assigneeReq AssigneeReq
+ if err := c.ShouldBindJSON(&assigneeReq); err != nil {
+ log.Print(err)
+ c.JSON(400, gin.H{
+ "error": "Invalid request",
+ })
+ return
+ }
+ chore, err := h.choreRepo.GetChore(c, id)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error getting chore",
+ })
+ return
+ }
+ // confirm that the assignee is one of the assignees:
+ assigneeFound := false
+ for _, assignee := range chore.Assignees {
+
+ if assignee.UserID == assigneeReq.AssignedTo {
+ assigneeFound = true
+ break
+ }
+ }
+ if !assigneeFound {
+ c.JSON(400, gin.H{
+ "error": "Assignee not found in assignees",
+ })
+ return
+ }
+
+ chore.UpdatedBy = assigneeReq.UpdatedBy
+ chore.AssignedTo = assigneeReq.AssignedTo
+ if err := h.choreRepo.UpsertChore(c, chore); err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error updating assignee",
+ })
+ return
+ }
+
+ c.JSON(200, gin.H{
+ "res": chore,
+ })
+}
+
+func (h *Handler) skipChore(c *gin.Context) {
+ rawID := c.Param("id")
+ id, err := strconv.Atoi(rawID)
+ if err != nil {
+ c.JSON(400, gin.H{
+ "error": "Invalid ID",
+ })
+ return
+ }
+
+ chore, err := h.choreRepo.GetChore(c, id)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error getting chore",
+ })
+ return
+ }
+ newDueDate, err := scheduleNextDueDate(chore, chore.NextDueDate.UTC())
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error scheduling next due date",
+ })
+ return
+ }
+ chore.NextDueDate = newDueDate
+ if err := h.choreRepo.UpsertChore(c, chore); err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error skipping chore",
+ })
+ return
+ }
+ if err := h.choreRepo.UpsertChore(c, chore); err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error skipping chore",
+ })
+ return
+ }
+ c.JSON(200, gin.H{
+ "res": chore,
+ })
+}
+
+func (h *Handler) updateDueDate(c *gin.Context) {
+ currentUser, ok := auth.CurrentUser(c)
+ if !ok {
+ c.JSON(500, gin.H{
+ "error": "Error getting current user",
+ })
+ return
+ }
+
+ type DueDateReq struct {
+ DueDate string `json:"dueDate" binding:"required"`
+ }
+
+ var dueDateReq DueDateReq
+ if err := c.ShouldBindJSON(&dueDateReq); err != nil {
+ log.Print(err)
+ c.JSON(400, gin.H{
+ "error": "Invalid request",
+ })
+ return
+ }
+
+ rawID := c.Param("id")
+ id, err := strconv.Atoi(rawID)
+ if err != nil {
+ c.JSON(400, gin.H{
+ "error": "Invalid ID",
+ })
+ return
+ }
+
+ rawDueDate, err := time.Parse(time.RFC3339, dueDateReq.DueDate)
+ if err != nil {
+ c.JSON(400, gin.H{
+ "error": "Invalid date",
+ })
+ return
+ }
+ dueDate := rawDueDate.UTC()
+ chore, err := h.choreRepo.GetChore(c, id)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error getting chore",
+ })
+ return
+ }
+ chore.NextDueDate = &dueDate
+ chore.UpdatedBy = currentUser.ID
+ if err := h.choreRepo.UpsertChore(c, chore); err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error updating due date",
+ })
+ return
+ }
+
+ c.JSON(200, gin.H{
+ "res": chore,
+ })
+}
+func (h *Handler) completeChore(c *gin.Context) {
+ type CompleteChoreReq struct {
+ Note string `json:"note"`
+ }
+ var req CompleteChoreReq
+ currentUser, ok := auth.CurrentUser(c)
+ if !ok {
+ c.JSON(500, gin.H{
+ "error": "Error getting current user",
+ })
+ return
+ }
+ completeChoreID := c.Param("id")
+ var completedDate time.Time
+ rawCompletedDate := c.Query("completedDate")
+ if rawCompletedDate == "" {
+ completedDate = time.Now().UTC()
+ } else {
+ var err error
+ completedDate, err = time.Parse(time.RFC3339, rawCompletedDate)
+ if err != nil {
+ c.JSON(400, gin.H{
+ "error": "Invalid date",
+ })
+ return
+ }
+ }
+
+ var additionalNotes *string
+ _ = c.ShouldBind(&req)
+
+ if req.Note != "" {
+ additionalNotes = &req.Note
+ }
+
+ id, err := strconv.Atoi(completeChoreID)
+ if err != nil {
+ c.JSON(400, gin.H{
+ "error": "Invalid ID",
+ })
+ return
+ }
+ chore, err := h.choreRepo.GetChore(c, id)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error getting chore",
+ })
+ 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
+ }
+ choreHistory, err := h.choreRepo.GetChoreHistory(c, chore.ID)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error getting chore history",
+ })
+ return
+ }
+
+ nextAssignedTo, err := checkNextAssignee(chore, choreHistory, currentUser.ID)
+ if err != nil {
+ log.Printf("Error checking next assignee: %s", err)
+ c.JSON(500, gin.H{
+ "error": "Error checking next assignee",
+ })
+ return
+ }
+
+ if err := h.choreRepo.CompleteChore(c, chore, additionalNotes, currentUser.ID, nextDueDate, completedDate, nextAssignedTo); err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error completing chore",
+ })
+ return
+ }
+ updatedChore, err := h.choreRepo.GetChore(c, id)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error getting chore",
+ })
+ return
+ }
+ go func() {
+ h.notifier.SendChoreCompletion(c, chore, []*uModel.User{currentUser})
+ h.nPlanner.GenerateNotifications(c, updatedChore)
+ }()
+ c.JSON(200, gin.H{
+ "res": updatedChore,
+ })
+}
+
+func (h *Handler) GetChoreHistory(c *gin.Context) {
+ rawID := c.Param("id")
+ id, err := strconv.Atoi(rawID)
+ if err != nil {
+ c.JSON(400, gin.H{
+ "error": "Invalid ID",
+ })
+ return
+ }
+
+ choreHistory, err := h.choreRepo.GetChoreHistory(c, id)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "Error getting chore history",
+ })
+ return
+ }
+
+ c.JSON(200, gin.H{
+ "res": choreHistory,
+ })
+}
+
+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))
+ copy(history, choresHistory)
+
+ assigneesMap := map[int]bool{}
+ for _, assignee := range chore.Assignees {
+ assigneesMap[assignee.UserID] = true
+ }
+ var nextAssignee int
+ if len(history) == 0 {
+ // if there is no history, just assume the current operation as the first
+ history = append(history, &chModel.ChoreHistory{
+ AssignedTo: performerID,
+ })
+
+ }
+
+ switch chore.AssignStrategy {
+ case "least_assigned":
+ // find the assignee with the least number of chores
+ assigneeChores := map[int]int{}
+ for _, performer := range chore.Assignees {
+ assigneeChores[performer.UserID] = 0
+ }
+ for _, history := range history {
+ if ok := assigneesMap[history.AssignedTo]; !ok {
+ // calculate the number of chores assigned to each assignee
+ assigneeChores[history.AssignedTo]++
+ }
+ }
+
+ minChores := math.MaxInt64
+ for assignee, numChores := range assigneeChores {
+ // if this is the first assignee or if the number of
+ // chores assigned to this assignee is less than the current minimum
+ if numChores < minChores {
+ minChores = numChores
+ // set the next assignee to this assignee
+ nextAssignee = assignee
+ }
+ }
+ case "least_completed":
+ // find the assignee who has completed the least number of chores
+ assigneeChores := map[int]int{}
+ for _, performer := range chore.Assignees {
+ assigneeChores[performer.UserID] = 0
+ }
+ for _, history := range history {
+ // calculate the number of chores completed by each assignee
+ assigneeChores[history.CompletedBy]++
+ }
+
+ // max Int value
+ minChores := math.MaxInt64
+ for assignee, numChores := range assigneeChores {
+ // if this is the first assignee or if the number of
+ // chores completed by this assignee is less than the current minimum
+ if numChores < minChores {
+ minChores = numChores
+ // set the next assignee to this assignee
+ nextAssignee = assignee
+ }
+ }
+ case "random":
+ nextAssignee = chore.Assignees[rand.Intn(len(chore.Assignees))].UserID
+ case "keep_last_assigned":
+ // keep the last assignee
+ nextAssignee = history[len(history)-1].AssignedTo
+
+ default:
+ return chore.AssignedTo, fmt.Errorf("invalid assign strategy")
+
+ }
+ return nextAssignee, nil
+}
+
+func Routes(router *gin.Engine, h *Handler, auth *jwt.GinJWTMiddleware) {
+
+ choresRoutes := router.Group("chores")
+ choresRoutes.Use(auth.MiddlewareFunc())
+ {
+ choresRoutes.GET("/", h.getChores)
+ choresRoutes.PUT("/", h.editChore)
+ choresRoutes.POST("/", h.createChore)
+ choresRoutes.GET("/:id", h.getChore)
+ choresRoutes.GET("/:id/history", h.GetChoreHistory)
+ choresRoutes.POST("/:id/do", h.completeChore)
+ choresRoutes.POST("/:id/skip", h.skipChore)
+ choresRoutes.PUT("/:id/assignee", h.updateAssignee)
+ choresRoutes.PUT("/:id/dueDate", h.updateDueDate)
+ choresRoutes.DELETE("/:id", h.deleteChore)
+ }
+
+}