Compare commits
19 Commits
19/post-al
...
f9fb956efd
| Author | SHA1 | Date | |
|---|---|---|---|
| f9fb956efd | |||
| 5a233073c7 | |||
| cd23e72882 | |||
| a82040720a | |||
| a8e3332723 | |||
| f8d1f195de | |||
| 426e456ba7 | |||
|
|
93ec3cbc19 | ||
|
|
5bcbde46ea | ||
|
|
f04529067a | ||
|
|
6e07d44733 | ||
|
|
1f21924805 | ||
|
|
e85a60ab16 | ||
|
|
61694e340f | ||
|
|
f72cc8a802 | ||
| da17f351de | |||
| 79dcfbc02c | |||
| 505faa95a3 | |||
|
|
a675d51718 |
24
.gitea/workflows/build.yml
Normal file
24
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: Backend Build and Test
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: standard-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '>=1.24'
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
cd backend
|
||||||
|
go build .
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: |
|
||||||
|
cd backend
|
||||||
|
go test . -v
|
||||||
27
.gitea/workflows/deploy.yml
Normal file
27
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: Backend Deploy
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: standard-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Login
|
||||||
|
with:
|
||||||
|
package_rw: ${{ secrets.PACKAGE_RW }}
|
||||||
|
run: docker login gitea.seeseepuff.be -u seeseemelk -p ${{ secrets.PACKAGE_RW }}
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
cd backend
|
||||||
|
docker build -t gitea.seeseepuff.be/seeseemelk/allowance-planner:$(git rev-parse --short HEAD) .
|
||||||
|
|
||||||
|
- name: Push
|
||||||
|
run: |
|
||||||
|
cd backend
|
||||||
|
docker push gitea.seeseepuff.be/seeseemelk/allowance-planner:$(git rev-parse --short HEAD)
|
||||||
@@ -2,8 +2,15 @@
|
|||||||
An improved Allowance Planner app.
|
An improved Allowance Planner app.
|
||||||
|
|
||||||
## Running backend
|
## Running backend
|
||||||
In order to run the backend, go to the `backend directory and run:
|
In order to run the backend, go to the `backend` directory and run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ go run .
|
$ go run .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Running frontend
|
||||||
|
In order to run the frontend, go to the `allowance-planner-v2` directory in the `frontend` directory and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ionic serve
|
||||||
|
```
|
||||||
|
|||||||
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
*.db3
|
*.db3
|
||||||
*.db3-*
|
*.db3-*
|
||||||
|
/allowance_planner
|
||||||
|
|||||||
14
backend/Dockerfile
Normal file
14
backend/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
FROM golang:1.24.2-alpine3.21
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY migrations ./migrations/
|
||||||
|
COPY *.go ./
|
||||||
|
COPY *.gohtml ./
|
||||||
|
RUN go build -o /allowance_planner
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
ENV GIN_MODE=release
|
||||||
|
CMD ["/allowance_planner"]
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TestAllowanceName = "Test History"
|
TestHistoryName = "Test History"
|
||||||
)
|
)
|
||||||
|
|
||||||
func startServer(t *testing.T) *httpexpect.Expect {
|
func startServer(t *testing.T) *httpexpect.Expect {
|
||||||
@@ -62,7 +62,7 @@ func TestGetUserAllowance(t *testing.T) {
|
|||||||
|
|
||||||
// Create a new allowance
|
// Create a new allowance
|
||||||
requestBody := map[string]interface{}{
|
requestBody := map[string]interface{}{
|
||||||
"name": TestAllowanceName,
|
"name": TestHistoryName,
|
||||||
"target": 5000,
|
"target": 5000,
|
||||||
"weight": 10,
|
"weight": 10,
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,7 @@ func TestGetUserAllowance(t *testing.T) {
|
|||||||
result.Length().IsEqual(2)
|
result.Length().IsEqual(2)
|
||||||
item := result.Value(1).Object()
|
item := result.Value(1).Object()
|
||||||
item.Value("id").IsEqual(1)
|
item.Value("id").IsEqual(1)
|
||||||
item.Value("name").IsEqual(TestAllowanceName)
|
item.Value("name").IsEqual(TestHistoryName)
|
||||||
item.Value("target").IsEqual(5000)
|
item.Value("target").IsEqual(5000)
|
||||||
item.Value("weight").IsEqual(10)
|
item.Value("weight").IsEqual(10)
|
||||||
item.Value("progress").IsEqual(0)
|
item.Value("progress").IsEqual(0)
|
||||||
@@ -95,7 +95,7 @@ func TestCreateUserAllowance(t *testing.T) {
|
|||||||
|
|
||||||
// Create a new allowance
|
// Create a new allowance
|
||||||
requestBody := map[string]interface{}{
|
requestBody := map[string]interface{}{
|
||||||
"name": TestAllowanceName,
|
"name": TestHistoryName,
|
||||||
"target": 5000,
|
"target": 5000,
|
||||||
"weight": 10,
|
"weight": 10,
|
||||||
}
|
}
|
||||||
@@ -120,7 +120,7 @@ func TestCreateUserAllowance(t *testing.T) {
|
|||||||
|
|
||||||
allowance := allowances.Value(1).Object()
|
allowance := allowances.Value(1).Object()
|
||||||
allowance.Value("id").IsEqual(allowanceId)
|
allowance.Value("id").IsEqual(allowanceId)
|
||||||
allowance.Value("name").IsEqual(TestAllowanceName)
|
allowance.Value("name").IsEqual(TestHistoryName)
|
||||||
allowance.Value("target").IsEqual(5000)
|
allowance.Value("target").IsEqual(5000)
|
||||||
allowance.Value("weight").IsEqual(10)
|
allowance.Value("weight").IsEqual(10)
|
||||||
allowance.Value("progress").IsEqual(0)
|
allowance.Value("progress").IsEqual(0)
|
||||||
@@ -130,7 +130,7 @@ func TestCreateUserAllowanceNoUser(t *testing.T) {
|
|||||||
e := startServer(t)
|
e := startServer(t)
|
||||||
|
|
||||||
requestBody := map[string]interface{}{
|
requestBody := map[string]interface{}{
|
||||||
"name": TestAllowanceName,
|
"name": TestHistoryName,
|
||||||
"target": 5000,
|
"target": 5000,
|
||||||
"weight": 10,
|
"weight": 10,
|
||||||
}
|
}
|
||||||
@@ -171,7 +171,7 @@ func TestCreateUserAllowanceBadId(t *testing.T) {
|
|||||||
e := startServer(t)
|
e := startServer(t)
|
||||||
|
|
||||||
requestBody := map[string]interface{}{
|
requestBody := map[string]interface{}{
|
||||||
"name": TestAllowanceName,
|
"name": TestHistoryName,
|
||||||
"target": 5000,
|
"target": 5000,
|
||||||
"weight": 10,
|
"weight": 10,
|
||||||
}
|
}
|
||||||
@@ -187,7 +187,7 @@ func TestDeleteUserAllowance(t *testing.T) {
|
|||||||
|
|
||||||
// Create a new allowance to delete
|
// Create a new allowance to delete
|
||||||
createRequest := map[string]interface{}{
|
createRequest := map[string]interface{}{
|
||||||
"name": TestAllowanceName,
|
"name": TestHistoryName,
|
||||||
"target": 1000,
|
"target": 1000,
|
||||||
"weight": 5,
|
"weight": 5,
|
||||||
}
|
}
|
||||||
@@ -434,7 +434,7 @@ func TestPutTaskInvalidTaskId(t *testing.T) {
|
|||||||
e.PUT("/task/999").WithJSON(requestBody).Expect().Status(404)
|
e.PUT("/task/999").WithJSON(requestBody).Expect().Status(404)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPostAllowance(t *testing.T) {
|
func TestPostHistory(t *testing.T) {
|
||||||
e := startServer(t)
|
e := startServer(t)
|
||||||
|
|
||||||
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 100}).Expect().Status(200)
|
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 100}).Expect().Status(200)
|
||||||
@@ -445,7 +445,7 @@ func TestPostAllowance(t *testing.T) {
|
|||||||
response.Value("allowance").Number().IsEqual(100 + 20 - 10)
|
response.Value("allowance").Number().IsEqual(100 + 20 - 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPostAllowanceInvalidUserId(t *testing.T) {
|
func TestPostHistoryInvalidUserId(t *testing.T) {
|
||||||
e := startServer(t)
|
e := startServer(t)
|
||||||
|
|
||||||
e.POST("/user/999/history").WithJSON(PostHistory{Allowance: 100}).Expect().
|
e.POST("/user/999/history").WithJSON(PostHistory{Allowance: 100}).Expect().
|
||||||
@@ -472,9 +472,10 @@ func TestGetUserAllowanceById(t *testing.T) {
|
|||||||
|
|
||||||
// Create a new allowance
|
// Create a new allowance
|
||||||
requestBody := map[string]interface{}{
|
requestBody := map[string]interface{}{
|
||||||
"name": TestAllowanceName,
|
"name": TestHistoryName,
|
||||||
"target": 5000,
|
"target": 5000,
|
||||||
"weight": 10,
|
"weight": 10,
|
||||||
|
"colour": "#FF5733",
|
||||||
}
|
}
|
||||||
resp := e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201).JSON().Object()
|
resp := e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201).JSON().Object()
|
||||||
allowanceId := int(resp.Value("id").Number().Raw())
|
allowanceId := int(resp.Value("id").Number().Raw())
|
||||||
@@ -482,10 +483,21 @@ func TestGetUserAllowanceById(t *testing.T) {
|
|||||||
// Retrieve the created allowance by ID
|
// Retrieve the created allowance by ID
|
||||||
result := e.GET("/user/1/allowance/" + strconv.Itoa(allowanceId)).Expect().Status(200).JSON().Object()
|
result := e.GET("/user/1/allowance/" + strconv.Itoa(allowanceId)).Expect().Status(200).JSON().Object()
|
||||||
result.Value("id").IsEqual(allowanceId)
|
result.Value("id").IsEqual(allowanceId)
|
||||||
result.Value("name").IsEqual(TestAllowanceName)
|
result.Value("name").IsEqual(TestHistoryName)
|
||||||
result.Value("target").IsEqual(5000)
|
result.Value("target").IsEqual(5000)
|
||||||
result.Value("weight").IsEqual(10)
|
result.Value("weight").IsEqual(10)
|
||||||
result.Value("progress").IsEqual(0)
|
result.Value("progress").IsEqual(0)
|
||||||
|
result.Value("colour").IsEqual("#FF5733")
|
||||||
|
|
||||||
|
resultArray := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
|
||||||
|
resultArray.Length().IsEqual(2)
|
||||||
|
result = resultArray.Value(1).Object()
|
||||||
|
result.Value("id").IsEqual(allowanceId)
|
||||||
|
result.Value("name").IsEqual(TestHistoryName)
|
||||||
|
result.Value("target").IsEqual(5000)
|
||||||
|
result.Value("weight").IsEqual(10)
|
||||||
|
result.Value("progress").IsEqual(0)
|
||||||
|
result.Value("colour").IsEqual("#FF5733")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetUserByAllowanceIdInvalidAllowance(t *testing.T) {
|
func TestGetUserByAllowanceIdInvalidAllowance(t *testing.T) {
|
||||||
@@ -513,9 +525,10 @@ func TestPutAllowanceById(t *testing.T) {
|
|||||||
|
|
||||||
// Create a new allowance
|
// Create a new allowance
|
||||||
requestBody := map[string]interface{}{
|
requestBody := map[string]interface{}{
|
||||||
"name": TestAllowanceName,
|
"name": TestHistoryName,
|
||||||
"target": 5000,
|
"target": 5000,
|
||||||
"weight": 10,
|
"weight": 10,
|
||||||
|
"colour": "#FF5733",
|
||||||
}
|
}
|
||||||
resp := e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201).JSON().Object()
|
resp := e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201).JSON().Object()
|
||||||
allowanceId := int(resp.Value("id").Number().Raw())
|
allowanceId := int(resp.Value("id").Number().Raw())
|
||||||
@@ -525,6 +538,7 @@ func TestPutAllowanceById(t *testing.T) {
|
|||||||
"name": "Updated Allowance",
|
"name": "Updated Allowance",
|
||||||
"target": 6000,
|
"target": 6000,
|
||||||
"weight": 15,
|
"weight": 15,
|
||||||
|
"colour": "#3357FF",
|
||||||
}
|
}
|
||||||
e.PUT("/user/1/allowance/" + strconv.Itoa(allowanceId)).WithJSON(updateRequest).Expect().Status(200)
|
e.PUT("/user/1/allowance/" + strconv.Itoa(allowanceId)).WithJSON(updateRequest).Expect().Status(200)
|
||||||
|
|
||||||
@@ -534,6 +548,7 @@ func TestPutAllowanceById(t *testing.T) {
|
|||||||
result.Value("name").IsEqual("Updated Allowance")
|
result.Value("name").IsEqual("Updated Allowance")
|
||||||
result.Value("target").IsEqual(6000)
|
result.Value("target").IsEqual(6000)
|
||||||
result.Value("weight").IsEqual(15)
|
result.Value("weight").IsEqual(15)
|
||||||
|
result.Value("colour").IsEqual("#3357FF")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCompleteTask(t *testing.T) {
|
func TestCompleteTask(t *testing.T) {
|
||||||
@@ -549,12 +564,12 @@ func TestCompleteTask(t *testing.T) {
|
|||||||
// Create two allowance goals
|
// Create two allowance goals
|
||||||
e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
|
e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
|
||||||
Name: "Test Allowance 1",
|
Name: "Test Allowance 1",
|
||||||
Target: 1000,
|
Target: 100,
|
||||||
Weight: 50,
|
Weight: 50,
|
||||||
}).Expect().Status(201)
|
}).Expect().Status(201)
|
||||||
e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
|
e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
|
||||||
Name: "Test Allowance 1",
|
Name: "Test Allowance 1",
|
||||||
Target: 1000,
|
Target: 10,
|
||||||
Weight: 25,
|
Weight: 25,
|
||||||
}).Expect().Status(201)
|
}).Expect().Status(201)
|
||||||
|
|
||||||
@@ -568,11 +583,11 @@ func TestCompleteTask(t *testing.T) {
|
|||||||
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
|
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
|
||||||
allowances.Length().IsEqual(3)
|
allowances.Length().IsEqual(3)
|
||||||
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
|
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().InDelta(30.34, 0.01)
|
||||||
allowances.Value(1).Object().Value("id").Number().IsEqual(1)
|
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().InDelta(60.66, 0.01)
|
||||||
allowances.Value(2).Object().Value("id").Number().IsEqual(2)
|
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
|
// And also for user 2
|
||||||
allowances = e.GET("/user/2/allowance").Expect().Status(200).JSON().Array()
|
allowances = e.GET("/user/2/allowance").Expect().Status(200).JSON().Array()
|
||||||
@@ -696,7 +711,7 @@ func getDelta(base time.Time, delta float64) (time.Time, time.Time) {
|
|||||||
return start, end
|
return start, end
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTestAllowance(e *httpexpect.Expect, name string, target int, weight float64) {
|
func createTestAllowance(e *httpexpect.Expect, name string, target float64, weight float64) {
|
||||||
e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
|
e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
|
||||||
Name: name,
|
Name: name,
|
||||||
Target: target,
|
Target: target,
|
||||||
|
|||||||
34
backend/colour.go
Normal file
34
backend/colour.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ConvertStringToColour(colourStr string) (int, error) {
|
||||||
|
if len(colourStr) == 0 {
|
||||||
|
return 0xFF0000, nil // Default colour if no string is provided
|
||||||
|
}
|
||||||
|
if colourStr[0] == '#' {
|
||||||
|
colourStr = colourStr[1:]
|
||||||
|
}
|
||||||
|
if len(colourStr) != 6 && len(colourStr) != 3 {
|
||||||
|
return 0, errors.New("colour must be a valid hex string")
|
||||||
|
}
|
||||||
|
var colour int
|
||||||
|
_, err := fmt.Sscanf(colourStr, "%x", &colour)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid colour format: %v", err)
|
||||||
|
}
|
||||||
|
if len(colourStr) == 3 {
|
||||||
|
r := (colour & 0xF00) >> 8
|
||||||
|
g := (colour & 0x0F0) >> 4
|
||||||
|
b := (colour & 0x00F) >> 0
|
||||||
|
colour = (r << 16 << 4) | (g << 8 << 4) | (b << 0 << 4)
|
||||||
|
}
|
||||||
|
return colour, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConvertColourToString(colour int) string {
|
||||||
|
return fmt.Sprintf("#%06X", colour)
|
||||||
|
}
|
||||||
30
backend/colour_test.go
Normal file
30
backend/colour_test.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConvertStringToColourWithSign(t *testing.T) {
|
||||||
|
colour, err := ConvertStringToColour("#123456")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 0x123456, colour)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertStringToColourWithoutSign(t *testing.T) {
|
||||||
|
colour, err := ConvertStringToColour("123456")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 0x123456, colour)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertStringToColourWithSignThreeDigits(t *testing.T) {
|
||||||
|
colour, err := ConvertStringToColour("#ABC")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 0xA0B0C0, colour)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertStringToColourWithoutSignThreeDigits(t *testing.T) {
|
||||||
|
colour, err := ConvertStringToColour("ABC")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 0xA0B0C0, colour)
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
|
"math"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.seeseepuff.be/seeseemelk/mysqlite"
|
"gitea.seeseepuff.be/seeseemelk/mysqlite"
|
||||||
@@ -49,8 +50,10 @@ func (db *Db) GetUsers() ([]User, error) {
|
|||||||
func (db *Db) GetUser(id int) (*UserWithAllowance, error) {
|
func (db *Db) GetUser(id int) (*UserWithAllowance, error) {
|
||||||
user := &UserWithAllowance{}
|
user := &UserWithAllowance{}
|
||||||
|
|
||||||
|
var allowance int
|
||||||
err := db.db.Query("select u.id, u.name, (select ifnull(sum(h.amount), 0) from history h where h.user_id = u.id) from users u where u.id = ?").
|
err := db.db.Query("select u.id, u.name, (select ifnull(sum(h.amount), 0) from history h where h.user_id = u.id) from users u where u.id = ?").
|
||||||
Bind(id).ScanSingle(&user.ID, &user.Name, &user.Allowance)
|
Bind(id).ScanSingle(&user.ID, &user.Name, &allowance)
|
||||||
|
user.Allowance = float64(allowance) / 100.0
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -70,18 +73,24 @@ func (db *Db) UserExists(userId int) (bool, error) {
|
|||||||
func (db *Db) GetUserAllowances(userId int) ([]Allowance, error) {
|
func (db *Db) GetUserAllowances(userId int) ([]Allowance, error) {
|
||||||
allowances := make([]Allowance, 0)
|
allowances := make([]Allowance, 0)
|
||||||
var err error
|
var err error
|
||||||
|
var progress int64
|
||||||
|
|
||||||
totalAllowance := Allowance{}
|
totalAllowance := Allowance{}
|
||||||
err = db.db.Query("select balance, weight from users where id = ?").Bind(userId).ScanSingle(&totalAllowance.Progress, &totalAllowance.Weight)
|
err = db.db.Query("select balance, weight from users where id = ?").Bind(userId).ScanSingle(&progress, &totalAllowance.Weight)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
totalAllowance.Progress = float64(progress) / 100.0
|
||||||
allowances = append(allowances, totalAllowance)
|
allowances = append(allowances, totalAllowance)
|
||||||
|
|
||||||
for row := range db.db.Query("select id, name, target, balance, weight from allowances where user_id = ?").
|
for row := range db.db.Query("select id, name, target, balance, weight, colour from allowances where user_id = ?").
|
||||||
Bind(userId).Range(&err) {
|
Bind(userId).Range(&err) {
|
||||||
allowance := Allowance{}
|
allowance := Allowance{}
|
||||||
err = row.Scan(&allowance.ID, &allowance.Name, &allowance.Target, &allowance.Progress, &allowance.Weight)
|
var target, progress, colour int
|
||||||
|
err = row.Scan(&allowance.ID, &allowance.Name, &target, &progress, &allowance.Weight, &colour)
|
||||||
|
allowance.Target = float64(target) / 100.0
|
||||||
|
allowance.Progress = float64(progress) / 100.0
|
||||||
|
allowance.Colour = ConvertColourToString(colour)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -96,15 +105,22 @@ func (db *Db) GetUserAllowances(userId int) ([]Allowance, error) {
|
|||||||
func (db *Db) GetUserAllowanceById(userId int, allowanceId int) (*Allowance, error) {
|
func (db *Db) GetUserAllowanceById(userId int, allowanceId int) (*Allowance, error) {
|
||||||
allowance := &Allowance{}
|
allowance := &Allowance{}
|
||||||
if allowanceId == 0 {
|
if allowanceId == 0 {
|
||||||
|
var progress int64
|
||||||
err := db.db.Query("select balance, weight from users where id = ?").
|
err := db.db.Query("select balance, weight from users where id = ?").
|
||||||
Bind(userId).ScanSingle(&allowance.Progress, &allowance.Weight)
|
Bind(userId).ScanSingle(&progress, &allowance.Weight)
|
||||||
|
allowance.Progress = float64(progress) / 100.0
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err := db.db.Query("select id, name, target, balance, weight from allowances where user_id = ? and id = ?").
|
var target, progress int64
|
||||||
|
var colour int
|
||||||
|
err := db.db.Query("select id, name, target, balance, weight, colour from allowances where user_id = ? and id = ?").
|
||||||
Bind(userId, allowanceId).
|
Bind(userId, allowanceId).
|
||||||
ScanSingle(&allowance.ID, &allowance.Name, &allowance.Target, &allowance.Progress, &allowance.Weight)
|
ScanSingle(&allowance.ID, &allowance.Name, &target, &progress, &allowance.Weight, &colour)
|
||||||
|
allowance.Target = float64(target) / 100.0
|
||||||
|
allowance.Progress = float64(progress) / 100.0
|
||||||
|
allowance.Colour = ConvertColourToString(colour)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -128,9 +144,15 @@ func (db *Db) CreateAllowance(userId int, allowance *CreateAllowanceRequest) (in
|
|||||||
}
|
}
|
||||||
defer tx.MustRollback()
|
defer tx.MustRollback()
|
||||||
|
|
||||||
|
// Convert string colour to a valid hex format
|
||||||
|
colour, err := ConvertStringToColour(allowance.Colour)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
// Insert the new allowance
|
// Insert the new allowance
|
||||||
err = tx.Query("insert into allowances (user_id, name, target, weight) values (?, ?, ?, ?)").
|
err = tx.Query("insert into allowances (user_id, name, target, weight, colour) values (?, ?, ?, ?, ?)").
|
||||||
Bind(userId, allowance.Name, allowance.Target, allowance.Weight).
|
Bind(userId, allowance.Name, int(math.Round(allowance.Target*100.0)), allowance.Weight, colour).
|
||||||
Exec()
|
Exec()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -242,8 +264,14 @@ func (db *Db) UpdateAllowance(userId int, allowanceId int, allowance *UpdateAllo
|
|||||||
}
|
}
|
||||||
defer tx.MustRollback()
|
defer tx.MustRollback()
|
||||||
|
|
||||||
err = tx.Query("update allowances set name=?, target=?, weight=? where id = ? and user_id = ?").
|
colour, err := ConvertStringToColour(allowance.Colour)
|
||||||
Bind(allowance.Name, allowance.Target, allowance.Weight, allowanceId, userId).
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
target := int(math.Round(allowance.Target * 100.0))
|
||||||
|
err = tx.Query("update allowances set name=?, target=?, weight=?, colour=? where id = ? and user_id = ?").
|
||||||
|
Bind(allowance.Name, target, allowance.Weight, colour, allowanceId, userId).
|
||||||
Exec()
|
Exec()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -284,8 +312,9 @@ func (db *Db) CreateTask(task *CreateTaskRequest) (int, error) {
|
|||||||
defer tx.MustRollback()
|
defer tx.MustRollback()
|
||||||
|
|
||||||
// Insert the new task
|
// Insert the new task
|
||||||
|
reward := int(math.Round(task.Reward * 100.0))
|
||||||
err = tx.Query("insert into tasks (name, reward, assigned) values (?, ?, ?)").
|
err = tx.Query("insert into tasks (name, reward, assigned) values (?, ?, ?)").
|
||||||
Bind(task.Name, task.Reward, task.Assigned).
|
Bind(task.Name, reward, task.Assigned).
|
||||||
Exec()
|
Exec()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -314,7 +343,9 @@ func (db *Db) GetTasks() ([]Task, error) {
|
|||||||
|
|
||||||
for row := range db.db.Query("select id, name, reward, assigned from tasks").Range(&err) {
|
for row := range db.db.Query("select id, name, reward, assigned from tasks").Range(&err) {
|
||||||
task := Task{}
|
task := Task{}
|
||||||
err = row.Scan(&task.ID, &task.Name, &task.Reward, &task.Assigned)
|
var reward int64
|
||||||
|
err = row.Scan(&task.ID, &task.Name, &reward, &task.Assigned)
|
||||||
|
task.Reward = float64(reward) / 100.0
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -329,8 +360,10 @@ func (db *Db) GetTasks() ([]Task, error) {
|
|||||||
func (db *Db) GetTask(id int) (Task, error) {
|
func (db *Db) GetTask(id int) (Task, error) {
|
||||||
task := Task{}
|
task := Task{}
|
||||||
|
|
||||||
|
var reward int64
|
||||||
err := db.db.Query("select id, name, reward, assigned from tasks where id = ?").
|
err := db.db.Query("select id, name, reward, assigned from tasks where id = ?").
|
||||||
Bind(id).ScanSingle(&task.ID, &task.Name, &task.Reward, &task.Assigned)
|
Bind(id).ScanSingle(&task.ID, &task.Name, &reward, &task.Assigned)
|
||||||
|
task.Reward = float64(reward) / 100.0
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Task{}, err
|
return Task{}, err
|
||||||
}
|
}
|
||||||
@@ -369,8 +402,9 @@ func (db *Db) UpdateTask(id int, task *CreateTaskRequest) error {
|
|||||||
}
|
}
|
||||||
defer tx.MustRollback()
|
defer tx.MustRollback()
|
||||||
|
|
||||||
|
reward := int(math.Round(task.Reward * 100.0))
|
||||||
err = tx.Query("update tasks set name=?, reward=?, assigned=? where id = ?").
|
err = tx.Query("update tasks set name=?, reward=?, assigned=? where id = ?").
|
||||||
Bind(task.Name, task.Reward, task.Assigned, id).
|
Bind(task.Name, reward, task.Assigned, id).
|
||||||
Exec()
|
Exec()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -416,16 +450,20 @@ func (db *Db) CompleteTask(taskId int) error {
|
|||||||
|
|
||||||
if sumOfWeights > 0 {
|
if sumOfWeights > 0 {
|
||||||
// Distribute the reward to the allowances
|
// 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) {
|
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 int
|
var allowanceId, allowanceTarget, allowanceBalance int
|
||||||
var allowanceWeight float64
|
var allowanceWeight float64
|
||||||
err = allowanceRow.Scan(&allowanceId, &allowanceWeight)
|
err = allowanceRow.Scan(&allowanceId, &allowanceWeight, &allowanceTarget, &allowanceBalance)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the amount to add to the allowance
|
// Calculate the amount to add to the allowance
|
||||||
amount := int((allowanceWeight / sumOfWeights) * float64(remainingReward))
|
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
|
sumOfWeights -= allowanceWeight
|
||||||
err = tx.Query("update allowances set balance = balance + ? where id = ? and user_id = ?").
|
err = tx.Query("update allowances set balance = balance + ? where id = ? and user_id = ?").
|
||||||
Bind(amount, allowanceId, userId).Exec()
|
Bind(amount, allowanceId, userId).Exec()
|
||||||
@@ -460,8 +498,9 @@ func (db *Db) AddHistory(userId int, allowance *PostHistory) error {
|
|||||||
}
|
}
|
||||||
defer tx.MustRollback()
|
defer tx.MustRollback()
|
||||||
|
|
||||||
|
amount := int(math.Round(allowance.Allowance * 100.0))
|
||||||
err = tx.Query("insert into history (user_id, timestamp, amount) values (?, ?, ?)").
|
err = tx.Query("insert into history (user_id, timestamp, amount) values (?, ?, ?)").
|
||||||
Bind(userId, time.Now().Unix(), allowance.Allowance).
|
Bind(userId, time.Now().Unix(), amount).
|
||||||
Exec()
|
Exec()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -476,11 +515,12 @@ func (db *Db) GetHistory(userId int) ([]History, error) {
|
|||||||
for row := range db.db.Query("select amount, `timestamp` from history where user_id = ? order by `timestamp` desc").
|
for row := range db.db.Query("select amount, `timestamp` from history where user_id = ? order by `timestamp` desc").
|
||||||
Bind(userId).Range(&err) {
|
Bind(userId).Range(&err) {
|
||||||
allowance := History{}
|
allowance := History{}
|
||||||
var timestamp int64
|
var timestamp, amount int64
|
||||||
err = row.Scan(&allowance.Allowance, ×tamp)
|
err = row.Scan(&amount, ×tamp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
allowance.Allowance = float64(amount) / 100.0
|
||||||
allowance.Timestamp = time.Unix(timestamp, 0)
|
allowance.Timestamp = time.Unix(timestamp, 0)
|
||||||
history = append(history, allowance)
|
history = append(history, allowance)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,44 +10,47 @@ type User struct {
|
|||||||
type UserWithAllowance struct {
|
type UserWithAllowance struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Allowance int `json:"allowance"`
|
Allowance float64 `json:"allowance"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type History struct {
|
type History struct {
|
||||||
Allowance int `json:"allowance"`
|
Allowance float64 `json:"allowance"`
|
||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostHistory struct {
|
type PostHistory struct {
|
||||||
Allowance int `json:"allowance"`
|
Allowance float64 `json:"allowance"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Task represents a task in the system.
|
// Task represents a task in the system.
|
||||||
type Task struct {
|
type Task struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Reward int `json:"reward"`
|
Reward float64 `json:"reward"`
|
||||||
Assigned *int `json:"assigned"` // Pointer to allow null
|
Assigned *int `json:"assigned"` // Pointer to allow null
|
||||||
}
|
}
|
||||||
|
|
||||||
type Allowance struct {
|
type Allowance struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Target int `json:"target"`
|
Target float64 `json:"target"`
|
||||||
Progress int `json:"progress"`
|
Progress float64 `json:"progress"`
|
||||||
Weight float64 `json:"weight"`
|
Weight float64 `json:"weight"`
|
||||||
|
Colour string `json:"colour"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateAllowanceRequest struct {
|
type CreateAllowanceRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Target int `json:"target"`
|
Target float64 `json:"target"`
|
||||||
Weight float64 `json:"weight"`
|
Weight float64 `json:"weight"`
|
||||||
|
Colour string `json:"colour"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateAllowanceRequest struct {
|
type UpdateAllowanceRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Target int `json:"target"`
|
Target float64 `json:"target"`
|
||||||
Weight float64 `json:"weight"`
|
Weight float64 `json:"weight"`
|
||||||
|
Colour string `json:"colour"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BulkUpdateAllowanceRequest struct {
|
type BulkUpdateAllowanceRequest struct {
|
||||||
@@ -61,7 +64,7 @@ type CreateGoalResponse struct {
|
|||||||
|
|
||||||
type CreateTaskRequest struct {
|
type CreateTaskRequest struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
Reward int `json:"reward"`
|
Reward float64 `json:"reward"`
|
||||||
Assigned *int `json:"assigned"`
|
Assigned *int `json:"assigned"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -587,9 +587,14 @@ func start(ctx context.Context, config *ServerConfig) {
|
|||||||
defer db.db.MustClose()
|
defer db.db.MustClose()
|
||||||
|
|
||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
router.Use(cors.New(cors.Config{
|
|
||||||
AllowOrigins: []string{"*"},
|
corsConfig := cors.DefaultConfig()
|
||||||
}))
|
corsConfig.AllowAllOrigins = true
|
||||||
|
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)
|
||||||
|
|||||||
2
backend/migrations/2_add_colour.sql
Normal file
2
backend/migrations/2_add_colour.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
alter table allowances
|
||||||
|
add column colour integer;
|
||||||
228
backend/web.go
Normal file
228
backend/web.go
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
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
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadWebEndpoints(router *gin.Engine) {
|
||||||
|
router.LoadHTMLFiles("web.gohtml")
|
||||||
|
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) {
|
||||||
|
if c.Query("user") != "" {
|
||||||
|
c.SetCookie("user", c.Query("user"), 3600, "/", "localhost", false, true)
|
||||||
|
}
|
||||||
|
c.Redirect(http.StatusFound, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderIndex(c *gin.Context) {
|
||||||
|
currentUser := getCurrentUser(c)
|
||||||
|
if currentUser == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
renderWithUser(c, *currentUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderCreateTask(c *gin.Context) {
|
||||||
|
currentUser := getCurrentUser(c)
|
||||||
|
if currentUser == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
name := c.PostForm("name")
|
||||||
|
rewardStr := c.PostForm("reward")
|
||||||
|
reward, err := strconv.ParseFloat(rewardStr, 64)
|
||||||
|
if err != nil {
|
||||||
|
renderError(c, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if name == "" || reward <= 0 {
|
||||||
|
renderError(c, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.CreateTask(&CreateTaskRequest{
|
||||||
|
Name: name,
|
||||||
|
Reward: reward,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
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.ParseFloat(targetStr, 64)
|
||||||
|
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, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderNoUser(c *gin.Context) {
|
||||||
|
users, err := db.GetUsers()
|
||||||
|
if err != nil {
|
||||||
|
renderError(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "web.gohtml", ViewModel{
|
||||||
|
Users: users,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderWithUser(c *gin.Context, currentUser int) {
|
||||||
|
users, err := db.GetUsers()
|
||||||
|
if err != nil {
|
||||||
|
renderError(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allowances, err := db.GetUserAllowances(currentUser)
|
||||||
|
if err != nil {
|
||||||
|
renderError(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks, err := db.GetTasks()
|
||||||
|
if err != nil {
|
||||||
|
renderError(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
history, err := db.GetHistory(currentUser)
|
||||||
|
if err != nil {
|
||||||
|
renderError(c, http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "web.gohtml", ViewModel{
|
||||||
|
Users: users,
|
||||||
|
CurrentUser: currentUser,
|
||||||
|
Allowances: allowances,
|
||||||
|
Tasks: tasks,
|
||||||
|
History: history,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderError(c *gin.Context, statusCode int, err error) {
|
||||||
|
c.HTML(statusCode, "web.gohtml", ViewModel{
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
132
backend/web.gohtml
Normal file
132
backend/web.gohtml
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
{{- /*gotype: allowance_planner.ViewModel*/}}
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Allowance Planner 2000</title>
|
||||||
|
<style>
|
||||||
|
tr:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Allowance Planner 2000</h1>
|
||||||
|
|
||||||
|
{{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}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{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>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>
|
||||||
@@ -11,6 +11,7 @@ const routes: Routes = [
|
|||||||
path: '',
|
path: '',
|
||||||
loadChildren: () => import('./pages/tabs/tabs.module').then(m => m.TabsPageModule)
|
loadChildren: () => import('./pages/tabs/tabs.module').then(m => m.TabsPageModule)
|
||||||
},
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import { BrowserModule } from '@angular/platform-browser';
|
|||||||
import { RouteReuseStrategy } from '@angular/router';
|
import { RouteReuseStrategy } from '@angular/router';
|
||||||
|
|
||||||
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
|
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
|
||||||
import { Drivers, Storage } from '@ionic/storage';
|
import { Drivers } from '@ionic/storage';
|
||||||
import { IonicStorageModule } from '@ionic/storage-angular';
|
import { IonicStorageModule } from '@ionic/storage-angular';
|
||||||
|
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [AppComponent],
|
declarations: [AppComponent],
|
||||||
@@ -15,6 +16,7 @@ import { AppComponent } from './app.component';
|
|||||||
BrowserModule,
|
BrowserModule,
|
||||||
IonicModule.forRoot(),
|
IonicModule.forRoot(),
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
IonicStorageModule.forRoot({
|
IonicStorageModule.forRoot({
|
||||||
name: '__mydb',
|
name: '__mydb',
|
||||||
driverOrder: [Drivers.IndexedDB, Drivers.LocalStorage]
|
driverOrder: [Drivers.IndexedDB, Drivers.LocalStorage]
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ export interface Task {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
reward: number;
|
reward: number;
|
||||||
assigned: number;
|
assigned: number | null;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { EditTaskPage } from './edit-task.page';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: EditTaskPage,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class EditTaskPageRoutingModule {}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
|
||||||
|
import { IonicModule } from '@ionic/angular';
|
||||||
|
|
||||||
|
import { EditTaskPageRoutingModule } from './edit-task-routing.module';
|
||||||
|
|
||||||
|
import { EditTaskPage } from './edit-task.page';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
IonicModule,
|
||||||
|
EditTaskPageRoutingModule,
|
||||||
|
ReactiveFormsModule
|
||||||
|
],
|
||||||
|
declarations: [EditTaskPage]
|
||||||
|
})
|
||||||
|
export class EditTaskPageModule {}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<ion-header [translucent]="true">
|
||||||
|
<ion-toolbar>
|
||||||
|
<div class="toolbar">
|
||||||
|
<ion-title *ngIf="isAddMode">Create Task</ion-title>
|
||||||
|
<ion-title *ngIf="!isAddMode">Edit Task</ion-title>
|
||||||
|
<button
|
||||||
|
*ngIf="!isAddMode"
|
||||||
|
class="remove-button"
|
||||||
|
(click)="deleteTask()"
|
||||||
|
>Delete task</button>
|
||||||
|
</div>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
|
||||||
|
<ion-content [fullscreen]="true">
|
||||||
|
<form [formGroup]="form">
|
||||||
|
<label>Task Name</label>
|
||||||
|
<input id="name" type="text" formControlName="name"/>
|
||||||
|
|
||||||
|
<label>Reward</label>
|
||||||
|
<input id="name" type="number" formControlName="reward"/>
|
||||||
|
|
||||||
|
<label>Assigned</label>
|
||||||
|
<select formControlName="assigned">
|
||||||
|
<option *ngFor="let user of users" [value]="user.id">{{ user.name }}</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button type="button" [disabled]="!form.valid" (click)="submit()">
|
||||||
|
<span *ngIf="isAddMode">Add Task</span>
|
||||||
|
<span *ngIf="!isAddMode">Update Task</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</ion-content>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-button {
|
||||||
|
background-color: var(--ion-color-primary);
|
||||||
|
margin-right: 15px;
|
||||||
|
width: 85px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
color: var(--ion-color-primary);
|
||||||
|
margin-top: 25px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
border: 1px solid var(--ion-color-primary);
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: var(--ion-color-primary);
|
||||||
|
border-radius: 5px;
|
||||||
|
color: white;
|
||||||
|
padding: 10px;
|
||||||
|
width: 250px;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled,
|
||||||
|
button[disabled]{
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { EditTaskPage } from './edit-task.page';
|
||||||
|
|
||||||
|
describe('EditTaskPage', () => {
|
||||||
|
let component: EditTaskPage;
|
||||||
|
let fixture: ComponentFixture<EditTaskPage>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(EditTaskPage);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { Location } from '@angular/common';
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { User } from 'src/app/models/user';
|
||||||
|
import { TaskService } from 'src/app/services/task.service';
|
||||||
|
import { UserService } from 'src/app/services/user.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-edit-task',
|
||||||
|
templateUrl: './edit-task.page.html',
|
||||||
|
styleUrls: ['./edit-task.page.scss'],
|
||||||
|
standalone: false,
|
||||||
|
})
|
||||||
|
export class EditTaskPage implements OnInit {
|
||||||
|
form: FormGroup;
|
||||||
|
id: number;
|
||||||
|
isAddMode: boolean;
|
||||||
|
users: Array<User> = [{id: 0, name: 'unassigned'}];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private taskService: TaskService,
|
||||||
|
private userService: UserService,
|
||||||
|
private router: Router
|
||||||
|
) {
|
||||||
|
this.id = this.route.snapshot.params['id'];
|
||||||
|
this.isAddMode = !this.id;
|
||||||
|
|
||||||
|
this.form = this.formBuilder.group({
|
||||||
|
name: ['', Validators.required],
|
||||||
|
reward: ['', [Validators.required, Validators.pattern("^[0-9]*$")]],
|
||||||
|
assigned: [0, Validators.required]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.userService.getUserList().subscribe(users => {
|
||||||
|
this.users.push(...users);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.isAddMode) {
|
||||||
|
this.taskService.getTaskById(this.id).subscribe(task => {
|
||||||
|
this.form.setValue({
|
||||||
|
name: task.name,
|
||||||
|
reward: task.reward,
|
||||||
|
assigned: task.assigned !== null ? task.assigned : 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
const formValue = this.form.value;
|
||||||
|
let assigned: number | null = Number(formValue.assigned);
|
||||||
|
if (assigned === 0) {
|
||||||
|
assigned = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const task = {
|
||||||
|
name: formValue.name,
|
||||||
|
reward: formValue.reward,
|
||||||
|
assigned
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isAddMode) {
|
||||||
|
this.taskService.createTask(task);
|
||||||
|
} else {
|
||||||
|
this.taskService.updateTask(this.id, task);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.router.navigate(['/tabs/tasks']);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTask() {
|
||||||
|
this.taskService.deleteTask(this.id);
|
||||||
|
this.router.navigate(['/tabs/tasks']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'tasks',
|
path: 'tasks',
|
||||||
loadChildren: () => import('../tasks/tasks.module').then(m => m.TasksPageModule)
|
loadChildren: () => import('../tasks/tasks.module').then(m => m.TasksPageModule),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: TasksPage,
|
component: TasksPage,
|
||||||
}
|
},
|
||||||
|
{ path: 'add', loadChildren: () => import('../edit-task/edit-task.module').then(m => m.EditTaskPageModule) },
|
||||||
|
{ path: 'edit/:id', loadChildren: () => import('../edit-task/edit-task.module').then(m => m.EditTaskPageModule) }
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
<ion-header [translucent]="true" class="ion-no-border">
|
<ion-header [translucent]="true" class="ion-no-border">
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
|
<div class="toolbar">
|
||||||
<ion-title>
|
<ion-title>
|
||||||
Tasks
|
Tasks
|
||||||
</ion-title>
|
</ion-title>
|
||||||
|
<button class="add-button" (click)="createTask()">Add task</button>
|
||||||
|
</div>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content>
|
<ion-content>
|
||||||
|
<div class="content">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<mat-icon>filter_alt</mat-icon>
|
<mat-icon>filter_alt</mat-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="list">
|
<div class="list">
|
||||||
<div class="task" *ngFor="let task of tasks">
|
<div class="task" *ngFor="let task of tasks$ | async">
|
||||||
<button>Done</button>
|
<button (click)="completeTask(task.id)">Done</button>
|
||||||
|
<div (click)="updateTask(task.id)" class="item">
|
||||||
<div class="name">{{ task.name }}</div>
|
<div class="name">{{ task.name }}</div>
|
||||||
<div
|
<div
|
||||||
class="reward"
|
class="reward"
|
||||||
@@ -20,4 +25,6 @@
|
|||||||
>{{ task.reward.toFixed(2) }} SP</div>
|
>{{ task.reward.toFixed(2) }} SP</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -23,6 +33,13 @@ mat-icon {
|
|||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
color: var(--font-color);
|
color: var(--font-color);
|
||||||
@@ -45,3 +62,9 @@ button {
|
|||||||
color: white;
|
color: white;
|
||||||
background: var(--confirm-button-color);
|
background: var(--confirm-button-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.add-button {
|
||||||
|
background-color: var(--ion-color-primary);
|
||||||
|
margin-right: 15px;
|
||||||
|
width: 75px;
|
||||||
|
}
|
||||||
@@ -1,24 +1,50 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||||
import { TaskService } from 'src/app/services/task.service';
|
import { TaskService } from 'src/app/services/task.service';
|
||||||
import { Task } from 'src/app/models/task';
|
import { Task } from 'src/app/models/task';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
import { ViewWillEnter } from '@ionic/angular';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-tasks',
|
selector: 'app-tasks',
|
||||||
templateUrl: 'tasks.page.html',
|
templateUrl: 'tasks.page.html',
|
||||||
styleUrls: ['tasks.page.scss'],
|
styleUrls: ['tasks.page.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: false,
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class TasksPage implements OnInit {
|
export class TasksPage implements ViewWillEnter {
|
||||||
public tasks: Array<Task> = [];
|
public tasks$: BehaviorSubject<Array<Task>> = new BehaviorSubject<Array<Task>>([]);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private taskService: TaskService
|
private taskService: TaskService,
|
||||||
) {}
|
private router: Router,
|
||||||
|
private route: ActivatedRoute
|
||||||
ngOnInit(): void {
|
) {
|
||||||
this.taskService.getTaskList().subscribe(tasks => {
|
this.getTasks();
|
||||||
this.tasks = tasks;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ionViewWillEnter(): void {
|
||||||
|
this.getTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
getTasks() {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.taskService.getTaskList().subscribe(tasks => {
|
||||||
|
this.tasks$.next(tasks);
|
||||||
|
});
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
createTask() {
|
||||||
|
this.router.navigate(['add'], { relativeTo: this.route });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTask(id: number) {
|
||||||
|
this.router.navigate(['edit', id], { relativeTo: this.route });
|
||||||
|
}
|
||||||
|
|
||||||
|
completeTask(id: number) {
|
||||||
|
this.taskService.completeTask(id);
|
||||||
|
this.getTasks();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,31 @@ import { Task } from '../models/task';
|
|||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class TaskService {
|
export class TaskService {
|
||||||
private url = 'http://localhost:8080/api'
|
private url = 'http://localhost:8080/api';
|
||||||
|
|
||||||
constructor(private http: HttpClient) {}
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
getTaskList(): Observable<Array<Task>> {
|
getTaskList(): Observable<Array<Task>> {
|
||||||
return this.http.get<Task[]>(`${this.url}/tasks`);
|
return this.http.get<Task[]>(`${this.url}/tasks`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTaskById(taskId: number): Observable<Task> {
|
||||||
|
return this.http.get<Task>(`${this.url}/task/${taskId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
createTask(task: Partial<Task>) {
|
||||||
|
this.http.post(`${this.url}/tasks`, task).subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTask(id: number, task: Partial<Task>) {
|
||||||
|
this.http.put(`${this.url}/task/${id}`, task).subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
completeTask(id: number) {
|
||||||
|
this.http.post(`${this.url}/task/${id}/complete`, {}).subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTask(id: number) {
|
||||||
|
this.http.delete(`${this.url}/task/${id}`).subscribe();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user