From 08edc867a72b1dcd1d77c8e88bd38c23140f6ce8 Mon Sep 17 00:00:00 2001 From: Sebastiaan de Schaetzen Date: Sat, 24 May 2025 06:09:44 +0200 Subject: [PATCH] First functional lite web thingy --- backend/api_test.go | 10 +-- backend/db.go | 10 ++- backend/web.go | 184 +++++++++++++++++++++++++++++--------------- backend/web.gohtml | 182 +++++++++++++++++++++++++------------------ 4 files changed, 242 insertions(+), 144 deletions(-) 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 @@ Allowance Planner 2000 +

Allowance Planner 2000

-

Users

-{{range .Users}} - {{if eq $.CurrentUser .ID}} - {{.Name}} - {{else}} - {{.Name}} - {{end}} -{{end}} -{{if ne .CurrentUser 0}} -

Allowances

- - - - - - - - - - - {{range .Allowances}} - {{if eq .ID 0}} - - - - - - - {{else}} - - - - - - - {{end}} +{{if ne .Error ""}} +

Error

+

{{.Error}}

+{{else}} +

Users

+ {{range .Users}} + {{if eq $.CurrentUser .ID}} + {{.Name}} + {{else}} + {{.Name}} {{end}} - -
NameProgressTargetWeight
Total{{.Progress}}{{.Weight}}
{{.Name}}{{.Progress}}{{.Target}}{{.Weight}}
+ {{end}} -

Tasks

-
+ {{if ne .CurrentUser 0}} +

Allowances

+ + + + + + + + + + + + + + + + + + + + {{range .Allowances}} + {{if eq .ID 0}} + + + + + + + {{else}} + + + + + + {{if ge .Progress .Target}} + + {{end}} + + {{end}} + {{end}} + +
NameProgressTargetWeightActions
Total{{.Progress}}{{.Weight}}
{{.Name}} ({{.Progress}}){{.Target}}{{.Weight}} + Mark as completed +
+
+ +

Tasks

+
+ + + + + + + + + + + {{range .Tasks}} + + + + + + + {{end}} + + + + + + + +
NameAssignedRewardActions
{{.Name}} + {{if eq .Assigned nil}} + None + {{else}} + {{.Assigned}} + {{end}} + {{.Reward}} + Mark as completed +
+
+ +

History

- - - + + - {{range .Tasks}} + {{range .History}} - - - + + {{end}} - - - - -
NameAssignedRewardTimestampAllowance
{{.Name}}{{.Assigned}}{{.Reward}}{{.Timestamp}}{{.Allowance}}
- - -
- - -

History

- - - - - - - - - {{range .History}} - - - - - {{end}} - -
TimestampAllowance
{{.Timestamp}}{{.Allowance}}
+ {{end}} {{end}}