Compare commits
7 Commits
icon
...
f4c8ae33dd
| Author | SHA1 | Date | |
|---|---|---|---|
| f4c8ae33dd | |||
| 885455454c | |||
| 5330cdd988 | |||
| 19292ec746 | |||
| 6a415ce878 | |||
| adff57bf29 | |||
| c4c2d42234 |
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
*.db3
|
*.db3
|
||||||
*.db3-*
|
*.db3-*
|
||||||
|
/allowance_planner
|
||||||
|
|||||||
@@ -623,38 +623,6 @@ func TestCompleteTaskInvalidId(t *testing.T) {
|
|||||||
e.POST("/task/999/complete").Expect().Status(404)
|
e.POST("/task/999/complete").Expect().Status(404)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCompleteTaskAllowanceWeightsSumTo0(t *testing.T) {
|
|
||||||
e := startServer(t)
|
|
||||||
taskId := createTestTaskWithAmount(e, 101)
|
|
||||||
|
|
||||||
e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)
|
|
||||||
|
|
||||||
// Update rest allowance
|
|
||||||
e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{
|
|
||||||
Weight: 0,
|
|
||||||
}).Expect().Status(200)
|
|
||||||
// Create two allowance goals
|
|
||||||
e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
|
|
||||||
Name: "Test Allowance 1",
|
|
||||||
Target: 1000,
|
|
||||||
Weight: 0,
|
|
||||||
}).Expect().Status(201)
|
|
||||||
|
|
||||||
// Complete the task
|
|
||||||
e.POST("/task/" + strconv.Itoa(taskId) + "/complete").Expect().Status(200)
|
|
||||||
|
|
||||||
// Verify the task is marked as completed
|
|
||||||
e.GET("/task/" + strconv.Itoa(taskId)).Expect().Status(404)
|
|
||||||
|
|
||||||
// Verify the allowances are updated for user 1
|
|
||||||
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
|
|
||||||
allowances.Length().IsEqual(2)
|
|
||||||
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
|
|
||||||
allowances.Value(0).Object().Value("progress").Number().IsEqual(101)
|
|
||||||
allowances.Value(1).Object().Value("id").Number().IsEqual(1)
|
|
||||||
allowances.Value(1).Object().Value("progress").Number().IsEqual(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteAllowance(t *testing.T) {
|
func TestCompleteAllowance(t *testing.T) {
|
||||||
e := startServer(t)
|
e := startServer(t)
|
||||||
createTestTaskWithAmount(e, 100)
|
createTestTaskWithAmount(e, 100)
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.12.0 h1:kl0VFgvm52UKxJhZpf1hvucxZdOoXY50g/VmzsWH+/8=
|
|
||||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.12.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
|
|
||||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.13.0 h1:nqSXu5i5fHB1rrx/kfi8Phn/J6eFa2yh02FiGc9U1yg=
|
|
||||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.13.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
|
|
||||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0 h1:aRItVfUj48fBmuec7rm/jY9KCfvHW2VzJfItVk4t8sw=
|
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0 h1:aRItVfUj48fBmuec7rm/jY9KCfvHW2VzJfItVk4t8sw=
|
||||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
|
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
|
||||||
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4=
|
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4=
|
||||||
@@ -218,14 +214,10 @@ modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8=
|
|||||||
modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
modernc.org/libc v1.65.6 h1:OhJUhmuJ6MVZdqL5qmnd0/my46DKGFhSX4WOR7ijfyE=
|
|
||||||
modernc.org/libc v1.65.6/go.mod h1:MOiGAM9lrMBT9L8xT1nO41qYl5eg9gCp9/kWhz5L7WA=
|
|
||||||
modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
|
modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
|
||||||
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
|
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
|
|
||||||
modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
|
|||||||
@@ -592,6 +592,9 @@ func start(ctx context.Context, config *ServerConfig) {
|
|||||||
corsConfig.AllowAllOrigins = true
|
corsConfig.AllowAllOrigins = true
|
||||||
router.Use(cors.New(corsConfig))
|
router.Use(cors.New(corsConfig))
|
||||||
|
|
||||||
|
// Web endpoints
|
||||||
|
loadWebEndpoints(router)
|
||||||
|
// API endpoints
|
||||||
router.GET("/api/users", getUsers)
|
router.GET("/api/users", getUsers)
|
||||||
router.GET("/api/user/:userId", getUser)
|
router.GET("/api/user/:userId", getUser)
|
||||||
router.POST("/api/user/:userId/history", postHistory)
|
router.POST("/api/user/:userId/history", postHistory)
|
||||||
@@ -641,7 +644,7 @@ func start(ctx context.Context, config *ServerConfig) {
|
|||||||
func main() {
|
func main() {
|
||||||
config := ServerConfig{
|
config := ServerConfig{
|
||||||
Datasource: os.Getenv("DB_PATH"),
|
Datasource: os.Getenv("DB_PATH"),
|
||||||
Addr: ":8080",
|
Addr: ":8081",
|
||||||
}
|
}
|
||||||
if config.Datasource == "" {
|
if config.Datasource == "" {
|
||||||
config.Datasource = "allowance_planner.db3"
|
config.Datasource = "allowance_planner.db3"
|
||||||
|
|||||||
168
backend/web.go
Normal file
168
backend/web.go
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ViewModel struct {
|
||||||
|
Users []User
|
||||||
|
CurrentUser int
|
||||||
|
Allowances []Allowance
|
||||||
|
Tasks []Task
|
||||||
|
History []History
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadWebEndpoints(router *gin.Engine) {
|
||||||
|
router.LoadHTMLFiles("web.gohtml")
|
||||||
|
router.GET("/", renderIndex)
|
||||||
|
router.GET("/login", renderLogin)
|
||||||
|
router.POST("/createTask", renderCreateTask)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderLogin(c *gin.Context) {
|
||||||
|
if c.Query("user") != "" {
|
||||||
|
c.SetCookie("user", c.Query("user"), 3600, "/", "localhost", false, true)
|
||||||
|
}
|
||||||
|
c.Redirect(http.StatusFound, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderIndex(c *gin.Context) {
|
||||||
|
currentUserStr, err := c.Cookie("user")
|
||||||
|
if errors.Is(err, http.ErrNoCookie) {
|
||||||
|
renderNoUser(c)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
name := c.PostForm("name")
|
||||||
|
rewardStr := c.PostForm("reward")
|
||||||
|
reward, err := strconv.Atoi(rewardStr)
|
||||||
|
if err != nil {
|
||||||
|
c.HTML(http.StatusBadRequest, "error.gohtml", gin.H{
|
||||||
|
"error": "Invalid reward value",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if name == "" || reward <= 0 {
|
||||||
|
c.HTML(http.StatusBadRequest, "error.gohtml", gin.H{
|
||||||
|
"error": "Name and reward must be provided",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.CreateTask(&CreateTaskRequest{
|
||||||
|
Name: name,
|
||||||
|
Reward: reward,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusFound, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func unsetUserCookie(c *gin.Context) {
|
||||||
|
c.SetCookie("user", "", -1, "/", "localhost", false, true)
|
||||||
|
c.Redirect(http.StatusFound, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderNoUser(c *gin.Context) {
|
||||||
|
users, err := db.GetUsers()
|
||||||
|
if err != nil {
|
||||||
|
c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "web.gohtml", ViewModel{
|
||||||
|
Users: users,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allowances, err := db.GetUserAllowances(currentUser)
|
||||||
|
if err != nil {
|
||||||
|
c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks, err := db.GetTasks()
|
||||||
|
if err != nil {
|
||||||
|
c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
history, err := db.GetHistory(currentUser)
|
||||||
|
if err != nil {
|
||||||
|
c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "web.gohtml", ViewModel{
|
||||||
|
Users: users,
|
||||||
|
CurrentUser: currentUser,
|
||||||
|
Allowances: allowances,
|
||||||
|
Tasks: tasks,
|
||||||
|
History: history,
|
||||||
|
})
|
||||||
|
}
|
||||||
98
backend/web.gohtml
Normal file
98
backend/web.gohtml
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
{{- /*gotype: allowance_planner.ViewModel*/}}
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Allowance Planner 2000</title>
|
||||||
|
</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}}
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Tasks</h2>
|
||||||
|
<form method="post" action="/createTask">
|
||||||
|
<table border="1">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Assigned</th>
|
||||||
|
<th>Reward</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .Tasks}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.Name}}</td>
|
||||||
|
<td>{{.Assigned}}</td>
|
||||||
|
<td>{{.Reward}}</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}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user