From c13dd9addbf89f716e4ef5cfdf1d673139ffcb68 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sun, 30 Jun 2024 21:41:41 -0400 Subject: Move to Donetick Org, first commit --- internal/chore/handler.go | 974 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 974 insertions(+) create mode 100644 internal/chore/handler.go (limited to 'internal/chore/handler.go') 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) + } + +} -- cgit