diff --git a/backend/api_test.go b/backend/api_test.go index e21c0e7..4bf31d4 100644 --- a/backend/api_test.go +++ b/backend/api_test.go @@ -549,12 +549,12 @@ func TestCompleteTask(t *testing.T) { // Create two allowance goals e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{ Name: "Test Allowance 1", - Target: 1000, + Target: 100, Weight: 50, }).Expect().Status(201) e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{ Name: "Test Allowance 1", - Target: 1000, + Target: 10, Weight: 25, }).Expect().Status(201) @@ -568,11 +568,11 @@ func TestCompleteTask(t *testing.T) { allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array() allowances.Length().IsEqual(3) allowances.Value(0).Object().Value("id").Number().IsEqual(0) - allowances.Value(0).Object().Value("progress").Number().IsEqual(26) + allowances.Value(0).Object().Value("progress").Number().IsEqual(31) allowances.Value(1).Object().Value("id").Number().IsEqual(1) - allowances.Value(1).Object().Value("progress").Number().IsEqual(50) + allowances.Value(1).Object().Value("progress").Number().IsEqual(60) allowances.Value(2).Object().Value("id").Number().IsEqual(2) - allowances.Value(2).Object().Value("progress").Number().IsEqual(25) + allowances.Value(2).Object().Value("progress").Number().IsEqual(10) // And also for user 2 allowances = e.GET("/user/2/allowance").Expect().Status(200).JSON().Array() diff --git a/backend/db.go b/backend/db.go index fe02ac2..0aa5570 100644 --- a/backend/db.go +++ b/backend/db.go @@ -416,16 +416,20 @@ func (db *Db) CompleteTask(taskId int) error { if sumOfWeights > 0 { // Distribute the reward to the allowances - for allowanceRow := range tx.Query("select id, weight from allowances where user_id = ? and weight > 0").Bind(userId).Range(&err) { - var allowanceId int + for allowanceRow := range tx.Query("select id, weight, target, balance from allowances where user_id = ? and weight > 0 order by (target - balance) asc").Bind(userId).Range(&err) { + var allowanceId, allowanceTarget, allowanceBalance int var allowanceWeight float64 - err = allowanceRow.Scan(&allowanceId, &allowanceWeight) + err = allowanceRow.Scan(&allowanceId, &allowanceWeight, &allowanceTarget, &allowanceBalance) if err != nil { return err } // Calculate the amount to add to the allowance amount := int((allowanceWeight / sumOfWeights) * float64(remainingReward)) + if allowanceBalance+amount > allowanceTarget { + // If the amount reaches past the target, set it to the target + amount = allowanceTarget - allowanceBalance + } sumOfWeights -= allowanceWeight err = tx.Query("update allowances set balance = balance + ? where id = ? and user_id = ?"). Bind(amount, allowanceId, userId).Exec() diff --git a/backend/web.go b/backend/web.go index 97b906a..12abbd0 100644 --- a/backend/web.go +++ b/backend/web.go @@ -13,6 +13,7 @@ type ViewModel struct { Allowances []Allowance Tasks []Task History []History + Error string } func loadWebEndpoints(router *gin.Engine) { @@ -20,6 +21,9 @@ func loadWebEndpoints(router *gin.Engine) { router.GET("/", renderIndex) router.GET("/login", renderLogin) router.POST("/createTask", renderCreateTask) + router.GET("/completeTask", renderCompleteTask) + router.POST("/createAllowance", renderCreateAllowance) + router.GET("/completeAllowance", renderCompleteAllowance) } func renderLogin(c *gin.Context) { @@ -30,49 +34,16 @@ func renderLogin(c *gin.Context) { } func renderIndex(c *gin.Context) { - currentUserStr, err := c.Cookie("user") - if errors.Is(err, http.ErrNoCookie) { - renderNoUser(c) + currentUser := getCurrentUser(c) + if currentUser == nil { return } - - if err != nil { - unsetUserCookie(c) - return - } - currentUser, err := strconv.Atoi(currentUserStr) - if err != nil { - unsetUserCookie(c) - return - } - userExists, err := db.UserExists(currentUser) - if !userExists || err != nil { - unsetUserCookie(c) - return - } - renderWithUser(c, currentUser) + renderWithUser(c, *currentUser) } func renderCreateTask(c *gin.Context) { - currentUserStr, err := c.Cookie("user") - if errors.Is(err, http.ErrNoCookie) { - c.HTML(http.StatusBadRequest, "error.gohtml", gin.H{ - "error": "User not logged in", - }) - return - } - if err != nil { - unsetUserCookie(c) - return - } - currentUser, err := strconv.Atoi(currentUserStr) - if err != nil { - unsetUserCookie(c) - return - } - userExists, err := db.UserExists(currentUser) - if !userExists || err != nil { - unsetUserCookie(c) + currentUser := getCurrentUser(c) + if currentUser == nil { return } @@ -80,15 +51,11 @@ func renderCreateTask(c *gin.Context) { rewardStr := c.PostForm("reward") reward, err := strconv.Atoi(rewardStr) if err != nil { - c.HTML(http.StatusBadRequest, "error.gohtml", gin.H{ - "error": "Invalid reward value", - }) + renderError(c, http.StatusBadRequest, err) return } if name == "" || reward <= 0 { - c.HTML(http.StatusBadRequest, "error.gohtml", gin.H{ - "error": "Name and reward must be provided", - }) + renderError(c, http.StatusBadRequest, err) return } @@ -97,15 +64,112 @@ func renderCreateTask(c *gin.Context) { Reward: reward, }) if err != nil { - c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{ - "error": err.Error(), - }) + renderError(c, http.StatusInternalServerError, err) return } c.Redirect(http.StatusFound, "/") } +func renderCompleteTask(c *gin.Context) { + taskIDStr := c.Query("task") + taskID, err := strconv.Atoi(taskIDStr) + if err != nil { + renderError(c, http.StatusBadRequest, err) + return + } + + err = db.CompleteTask(taskID) + if err != nil { + renderError(c, http.StatusInternalServerError, err) + return + } + + c.Redirect(http.StatusFound, "/") +} + +func renderCreateAllowance(c *gin.Context) { + currentUser := getCurrentUser(c) + if currentUser == nil { + return + } + + name := c.PostForm("name") + targetStr := c.PostForm("target") + target, err := strconv.Atoi(targetStr) + if err != nil { + renderError(c, http.StatusBadRequest, err) + return + } + weightStr := c.PostForm("weight") + weight, err := strconv.ParseFloat(weightStr, 64) + if err != nil { + renderError(c, http.StatusBadRequest, err) + return + } + if name == "" || target <= 0 || weight <= 0 { + renderError(c, http.StatusBadRequest, err) + return + } + + _, err = db.CreateAllowance(*currentUser, &CreateAllowanceRequest{ + Name: name, + Target: target, + Weight: weight, + }) + if err != nil { + renderError(c, http.StatusInternalServerError, err) + return + } + + c.Redirect(http.StatusFound, "/") +} + +func renderCompleteAllowance(c *gin.Context) { + currentUser := getCurrentUser(c) + if currentUser == nil { + return + } + + allowanceIDStr := c.Query("allowance") + allowanceID, err := strconv.Atoi(allowanceIDStr) + if err != nil { + renderError(c, http.StatusBadRequest, err) + return + } + + err = db.CompleteAllowance(*currentUser, allowanceID) + if err != nil { + renderError(c, http.StatusInternalServerError, err) + return + } + + c.Redirect(http.StatusFound, "/") +} + +func getCurrentUser(c *gin.Context) *int { + currentUserStr, err := c.Cookie("user") + if errors.Is(err, http.ErrNoCookie) { + renderNoUser(c) + return nil + } + if err != nil { + unsetUserCookie(c) + return nil + } + currentUser, err := strconv.Atoi(currentUserStr) + if err != nil { + unsetUserCookie(c) + return nil + } + userExists, err := db.UserExists(currentUser) + if !userExists || err != nil { + unsetUserCookie(c) + return nil + } + return ¤tUser +} + func unsetUserCookie(c *gin.Context) { c.SetCookie("user", "", -1, "/", "localhost", false, true) c.Redirect(http.StatusFound, "/") @@ -114,9 +178,7 @@ func unsetUserCookie(c *gin.Context) { func renderNoUser(c *gin.Context) { users, err := db.GetUsers() if err != nil { - c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{ - "error": err.Error(), - }) + renderError(c, http.StatusInternalServerError, err) return } @@ -128,33 +190,25 @@ func renderNoUser(c *gin.Context) { func renderWithUser(c *gin.Context, currentUser int) { users, err := db.GetUsers() if err != nil { - c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{ - "error": err.Error(), - }) + renderError(c, http.StatusInternalServerError, err) return } allowances, err := db.GetUserAllowances(currentUser) if err != nil { - c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{ - "error": err.Error(), - }) + renderError(c, http.StatusInternalServerError, err) return } tasks, err := db.GetTasks() if err != nil { - c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{ - "error": err.Error(), - }) + renderError(c, http.StatusInternalServerError, err) return } history, err := db.GetHistory(currentUser) if err != nil { - c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{ - "error": err.Error(), - }) + renderError(c, http.StatusInternalServerError, err) return } @@ -166,3 +220,9 @@ func renderWithUser(c *gin.Context, currentUser int) { History: history, }) } + +func renderError(c *gin.Context, statusCode int, err error) { + c.HTML(statusCode, "web.gohtml", ViewModel{ + Error: err.Error(), + }) +} diff --git a/backend/web.gohtml b/backend/web.gohtml index 11272a5..1ed49ee 100644 --- a/backend/web.gohtml +++ b/backend/web.gohtml @@ -2,97 +2,131 @@ <html lang="en"> <head> <title>Allowance Planner 2000</title> + <style> + tr:hover { + background-color: #f0f0f0; + } + </style> </head> <body> <h1>Allowance Planner 2000</h1> -<h2>Users</h2> -{{range .Users}} - {{if eq $.CurrentUser .ID}} - <strong>{{.Name}}</strong> - {{else}} - <a href="/login?user={{.ID}}">{{.Name}}</a> - {{end}} -{{end}} -{{if ne .CurrentUser 0}} - <h2>Allowances</h2> - <table border="1"> - <thead> - <tr> - <th>Name</th> - <th>Progress</th> - <th>Target</th> - <th>Weight</th> - </tr> - </thead> - <tbody> - {{range .Allowances}} - {{if eq .ID 0}} - <tr> - <td>Total</td> - <td>{{.Progress}}</td> - <td></td> - <td>{{.Weight}}</td> - </tr> - {{else}} - <tr> - <td>{{.Name}}</td> - <td>{{.Progress}}</td> - <td>{{.Target}}</td> - <td>{{.Weight}}</td> - </tr> - {{end}} +{{if ne .Error ""}} + <h2>Error</h2> + <p>{{.Error}}</p> +{{else}} + <h2>Users</h2> + {{range .Users}} + {{if eq $.CurrentUser .ID}} + <strong>{{.Name}}</strong> + {{else}} + <a href="/login?user={{.ID}}">{{.Name}}</a> {{end}} - </tbody> - </table> + {{end}} - <h2>Tasks</h2> - <form method="post" action="/createTask"> + {{if ne .CurrentUser 0}} + <h2>Allowances</h2> + <form action="/createAllowance" method="post"> + <table border="1"> + <thead> + <tr> + <th>Name</th> + <th>Progress</th> + <th>Target</th> + <th>Weight</th> + <th>Actions</th> + </tr> + </thead> + <tbody> + <tr> + <td><label><input type="text" name="name" placeholder="Name"></label></td> + <td></td> + <td><label><input type="number" name="target" placeholder="Target"></label></td> + <td><label><input type="number" name="weight" placeholder="Weight"></label></td> + <td><button>Create</button></td> + </tr> + {{range .Allowances}} + {{if eq .ID 0}} + <tr> + <td>Total</td> + <td>{{.Progress}}</td> + <td></td> + <td>{{.Weight}}</td> + </tr> + {{else}} + <tr> + <td>{{.Name}}</td> + <td><progress max="{{.Target}}" value="{{.Progress}}"></progress> ({{.Progress}})</td> + <td>{{.Target}}</td> + <td>{{.Weight}}</td> + {{if ge .Progress .Target}} + <td> + <a href="/completeAllowance?allowance={{.ID}}">Mark as completed</a> + </td> + {{end}} + </tr> + {{end}} + {{end}} + </tbody> + </table> + </form> + + <h2>Tasks</h2> + <form method="post" action="/createTask"> + <table border="1"> + <thead> + <tr> + <th>Name</th> + <th>Assigned</th> + <th>Reward</th> + <th>Actions</th> + </tr> + </thead> + <tbody> + {{range .Tasks}} + <tr> + <td>{{.Name}}</td> + <td> + {{if eq .Assigned nil}} + None + {{else}} + {{.Assigned}} + {{end}} + </td> + <td>{{.Reward}}</td> + <td> + <a href="/completeTask?task={{.ID}}">Mark as completed</a> + </td> + </tr> + {{end}} + <tr> + <td><label><input type="text" name="name" placeholder="Name"></label></td> + <td></td> + <td><label><input type="number" name="reward" placeholder="Reward"></label></td> + <td><button>Create</button></td> + </tr> + </tbody> + </table> + </form> + + <h2>History</h2> <table border="1"> <thead> <tr> - <th>Name</th> - <th>Assigned</th> - <th>Reward</th> + <th>Timestamp</th> + <th>Allowance</th> </tr> </thead> <tbody> - {{range .Tasks}} + {{range .History}} <tr> - <td>{{.Name}}</td> - <td>{{.Assigned}}</td> - <td>{{.Reward}}</td> + <td>{{.Timestamp}}</td> + <td>{{.Allowance}}</td> </tr> {{end}} - <tr> - <td><label><input type="text" placeholder="Name"></label></td> - <td></td> - <td> - <label><input type="number" placeholder="Reward"></label> - <button>Create</button> - </td> - </tr> </tbody> </table> - </form> - - <h2>History</h2> - <table border="1"> - <thead> - <tr> - <th>Timestamp</th> - <th>Allowance</th> - </tr> - </thead> - <tbody> - {{range .History}} - <tr> - <td>{{.Timestamp}}</td> - <td>{{.Allowance}}</td> - </tr> - {{end}} - </tbody> - </table> + {{end}} {{end}} </body> </html>