20 Commits

Author SHA1 Message Date
67dbcd053d Set default total allowance to 10
All checks were successful
Backend Build and Test / build (push) Successful in 2m5s
2025-05-25 14:58:32 +02:00
f9fb956efd Fix colour not being sent properly by backend (#81)
All checks were successful
Backend Deploy / build (push) Successful in 2m46s
Backend Build and Test / build (push) Successful in 3m0s
Reviewed-on: #81
2025-05-25 14:36:17 +02:00
5a233073c7 Do deploy on main branch (#80)
All checks were successful
Backend Build and Test / build (push) Successful in 3m21s
Backend Deploy / build (push) Successful in 4m3s
Reviewed-on: #80
2025-05-25 14:24:25 +02:00
cd23e72882 Allow null colour (#79)
Some checks failed
Backend Build and Test / build (push) Has been cancelled
Reviewed-on: #79
2025-05-25 14:22:58 +02:00
a82040720a Add gitea workflow (#78)
All checks were successful
Backend Build and Test / build (push) Successful in 2m9s
Reviewed-on: #78
2025-05-25 14:20:38 +02:00
a8e3332723 Add support for colour attribute on allowances in backend (#77)
Closes #76

Reviewed-on: #77
2025-05-25 13:43:24 +02:00
f8d1f195de Support decimal currency amounts (#74)
Reviewed-on: #74
2025-05-24 06:28:07 +02:00
426e456ba7 Add lite website (#73)
Reviewed-on: #73
2025-05-24 06:11:39 +02:00
Huffle
93ec3cbc19 Add delete task functionality (#72)
closes #62

Reviewed-on: #72
2025-05-19 09:43:11 +02:00
Huffle
5bcbde46ea Add complete task functionality (#71)
closes #61
closes #58

Reviewed-on: #71
2025-05-19 09:07:51 +02:00
Huffle
f04529067a update task functionality (#59)
Reviewed-on: #59
2025-05-18 16:43:53 +02:00
Huffle
6e07d44733 update task functionality 2025-05-18 16:34:49 +02:00
Huffle
1f21924805 AP-45 (#57)
Reviewed-on: #57
2025-05-18 16:18:07 +02:00
Huffle
e85a60ab16 Merge branch 'main' into AP-45 2025-05-18 16:17:13 +02:00
Huffle
61694e340f Add functionalty to add task 2025-05-18 16:15:34 +02:00
Huffle
f72cc8a802 test 2025-05-18 10:48:52 +02:00
da17f351de Add bulk allowance edit endpoint (#56)
Closes #15

Reviewed-on: #56
2025-05-18 09:24:36 +02:00
79dcfbc02c Implement completion endpoint for allowance (#55)
Closes #19

Reviewed-on: #55
2025-05-18 09:02:33 +02:00
505faa95a3 Add different cors config 2025-05-18 08:54:22 +02:00
Huffle
a675d51718 AP-45 wip post request 2025-05-18 08:52:20 +02:00
32 changed files with 985 additions and 97 deletions

View 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

View 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)

View File

@@ -2,8 +2,15 @@
An improved Allowance Planner app.
## 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
$ 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
View File

@@ -1,2 +1,3 @@
*.db3
*.db3-*
/allowance_planner

14
backend/Dockerfile Normal file
View 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"]

Binary file not shown.

View File

@@ -9,7 +9,7 @@ import (
)
const (
TestAllowanceName = "Test History"
TestHistoryName = "Test History"
)
func startServer(t *testing.T) *httpexpect.Expect {
@@ -62,7 +62,7 @@ func TestGetUserAllowance(t *testing.T) {
// Create a new allowance
requestBody := map[string]interface{}{
"name": TestAllowanceName,
"name": TestHistoryName,
"target": 5000,
"weight": 10,
}
@@ -73,7 +73,7 @@ func TestGetUserAllowance(t *testing.T) {
result.Length().IsEqual(2)
item := result.Value(1).Object()
item.Value("id").IsEqual(1)
item.Value("name").IsEqual(TestAllowanceName)
item.Value("name").IsEqual(TestHistoryName)
item.Value("target").IsEqual(5000)
item.Value("weight").IsEqual(10)
item.Value("progress").IsEqual(0)
@@ -95,7 +95,7 @@ func TestCreateUserAllowance(t *testing.T) {
// Create a new allowance
requestBody := map[string]interface{}{
"name": TestAllowanceName,
"name": TestHistoryName,
"target": 5000,
"weight": 10,
}
@@ -120,7 +120,7 @@ func TestCreateUserAllowance(t *testing.T) {
allowance := allowances.Value(1).Object()
allowance.Value("id").IsEqual(allowanceId)
allowance.Value("name").IsEqual(TestAllowanceName)
allowance.Value("name").IsEqual(TestHistoryName)
allowance.Value("target").IsEqual(5000)
allowance.Value("weight").IsEqual(10)
allowance.Value("progress").IsEqual(0)
@@ -130,7 +130,7 @@ func TestCreateUserAllowanceNoUser(t *testing.T) {
e := startServer(t)
requestBody := map[string]interface{}{
"name": TestAllowanceName,
"name": TestHistoryName,
"target": 5000,
"weight": 10,
}
@@ -171,7 +171,7 @@ func TestCreateUserAllowanceBadId(t *testing.T) {
e := startServer(t)
requestBody := map[string]interface{}{
"name": TestAllowanceName,
"name": TestHistoryName,
"target": 5000,
"weight": 10,
}
@@ -187,7 +187,7 @@ func TestDeleteUserAllowance(t *testing.T) {
// Create a new allowance to delete
createRequest := map[string]interface{}{
"name": TestAllowanceName,
"name": TestHistoryName,
"target": 1000,
"weight": 5,
}
@@ -434,7 +434,7 @@ func TestPutTaskInvalidTaskId(t *testing.T) {
e.PUT("/task/999").WithJSON(requestBody).Expect().Status(404)
}
func TestPostAllowance(t *testing.T) {
func TestPostHistory(t *testing.T) {
e := startServer(t)
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)
}
func TestPostAllowanceInvalidUserId(t *testing.T) {
func TestPostHistoryInvalidUserId(t *testing.T) {
e := startServer(t)
e.POST("/user/999/history").WithJSON(PostHistory{Allowance: 100}).Expect().
@@ -472,9 +472,10 @@ func TestGetUserAllowanceById(t *testing.T) {
// Create a new allowance
requestBody := map[string]interface{}{
"name": TestAllowanceName,
"name": TestHistoryName,
"target": 5000,
"weight": 10,
"colour": "#FF5733",
}
resp := e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201).JSON().Object()
allowanceId := int(resp.Value("id").Number().Raw())
@@ -482,10 +483,21 @@ func TestGetUserAllowanceById(t *testing.T) {
// Retrieve the created allowance by ID
result := e.GET("/user/1/allowance/" + strconv.Itoa(allowanceId)).Expect().Status(200).JSON().Object()
result.Value("id").IsEqual(allowanceId)
result.Value("name").IsEqual(TestAllowanceName)
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")
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) {
@@ -513,9 +525,10 @@ func TestPutAllowanceById(t *testing.T) {
// Create a new allowance
requestBody := map[string]interface{}{
"name": TestAllowanceName,
"name": TestHistoryName,
"target": 5000,
"weight": 10,
"colour": "#FF5733",
}
resp := e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201).JSON().Object()
allowanceId := int(resp.Value("id").Number().Raw())
@@ -525,6 +538,7 @@ func TestPutAllowanceById(t *testing.T) {
"name": "Updated Allowance",
"target": 6000,
"weight": 15,
"colour": "#3357FF",
}
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("target").IsEqual(6000)
result.Value("weight").IsEqual(15)
result.Value("colour").IsEqual("#3357FF")
}
func TestCompleteTask(t *testing.T) {
@@ -549,12 +564,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 +583,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().InDelta(30.34, 0.01)
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("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()
@@ -590,6 +605,36 @@ func TestCompleteTask(t *testing.T) {
}
}
func TestCompleteTaskWithNoWeights(t *testing.T) {
e := startServer(t)
taskId := createTestTaskWithAmount(e, 101)
e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)
// Ensure main allowance has no weight
e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{
Weight: 0,
}).Expect().Status(200)
// 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(1)
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
allowances.Value(0).Object().Value("progress").Number().InDelta(101.00, 0.01)
// And also for user 2
allowances = e.GET("/user/2/allowance").Expect().Status(200).JSON().Array()
allowances.Length().IsEqual(1)
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
allowances.Value(0).Object().Value("progress").Number().InDelta(101.00, 0.01)
}
func TestCompleteTaskAllowanceWeightsSumTo0(t *testing.T) {
e := startServer(t)
taskId := createTestTaskWithAmount(e, 101)
@@ -628,6 +673,11 @@ func TestCompleteAllowance(t *testing.T) {
createTestTaskWithAmount(e, 100)
createTestAllowance(e, "Test Allowance 1", 100, 50)
// Update base allowance
e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{
Weight: 0,
}).Expect().Status(200)
// Complete the task
e.POST("/task/1/complete").Expect().Status(200)
@@ -696,7 +746,7 @@ func getDelta(base time.Time, delta float64) (time.Time, time.Time) {
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{
Name: name,
Target: target,

34
backend/colour.go Normal file
View 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
View 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)
}

View File

@@ -3,6 +3,7 @@ package main
import (
"errors"
"log"
"math"
"time"
"gitea.seeseepuff.be/seeseemelk/mysqlite"
@@ -49,8 +50,10 @@ func (db *Db) GetUsers() ([]User, error) {
func (db *Db) GetUser(id int) (*UserWithAllowance, error) {
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 = ?").
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 {
return nil, err
}
@@ -70,18 +73,24 @@ func (db *Db) UserExists(userId int) (bool, error) {
func (db *Db) GetUserAllowances(userId int) ([]Allowance, error) {
allowances := make([]Allowance, 0)
var err error
var progress int64
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 {
return nil, err
}
totalAllowance.Progress = float64(progress) / 100.0
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) {
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 {
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) {
allowance := &Allowance{}
if allowanceId == 0 {
var progress int64
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 {
return nil, err
}
} 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).
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 {
return nil, err
}
@@ -128,9 +144,15 @@ func (db *Db) CreateAllowance(userId int, allowance *CreateAllowanceRequest) (in
}
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
err = tx.Query("insert into allowances (user_id, name, target, weight) values (?, ?, ?, ?)").
Bind(userId, allowance.Name, allowance.Target, allowance.Weight).
err = tx.Query("insert into allowances (user_id, name, target, weight, colour) values (?, ?, ?, ?, ?)").
Bind(userId, allowance.Name, int(math.Round(allowance.Target*100.0)), allowance.Weight, colour).
Exec()
if err != nil {
@@ -242,8 +264,14 @@ func (db *Db) UpdateAllowance(userId int, allowanceId int, allowance *UpdateAllo
}
defer tx.MustRollback()
err = tx.Query("update allowances set name=?, target=?, weight=? where id = ? and user_id = ?").
Bind(allowance.Name, allowance.Target, allowance.Weight, allowanceId, userId).
colour, err := ConvertStringToColour(allowance.Colour)
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()
if err != nil {
return err
@@ -284,8 +312,9 @@ func (db *Db) CreateTask(task *CreateTaskRequest) (int, error) {
defer tx.MustRollback()
// Insert the new task
reward := int(math.Round(task.Reward * 100.0))
err = tx.Query("insert into tasks (name, reward, assigned) values (?, ?, ?)").
Bind(task.Name, task.Reward, task.Assigned).
Bind(task.Name, reward, task.Assigned).
Exec()
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) {
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 {
return nil, err
}
@@ -329,8 +360,10 @@ func (db *Db) GetTasks() ([]Task, error) {
func (db *Db) GetTask(id int) (Task, error) {
task := Task{}
var reward int64
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 {
return Task{}, err
}
@@ -369,8 +402,9 @@ func (db *Db) UpdateTask(id int, task *CreateTaskRequest) error {
}
defer tx.MustRollback()
reward := int(math.Round(task.Reward * 100.0))
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()
if err != nil {
return err
@@ -416,16 +450,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()
@@ -460,8 +498,9 @@ func (db *Db) AddHistory(userId int, allowance *PostHistory) error {
}
defer tx.MustRollback()
amount := int(math.Round(allowance.Allowance * 100.0))
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()
if err != nil {
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").
Bind(userId).Range(&err) {
allowance := History{}
var timestamp int64
err = row.Scan(&allowance.Allowance, &timestamp)
var timestamp, amount int64
err = row.Scan(&amount, &timestamp)
if err != nil {
return nil, err
}
allowance.Allowance = float64(amount) / 100.0
allowance.Timestamp = time.Unix(timestamp, 0)
history = append(history, allowance)
}

View File

@@ -8,46 +8,49 @@ type User struct {
}
type UserWithAllowance struct {
ID int `json:"id"`
Name string `json:"name"`
Allowance int `json:"allowance"`
ID int `json:"id"`
Name string `json:"name"`
Allowance float64 `json:"allowance"`
}
type History struct {
Allowance int `json:"allowance"`
Allowance float64 `json:"allowance"`
Timestamp time.Time `json:"timestamp"`
}
type PostHistory struct {
Allowance int `json:"allowance"`
Allowance float64 `json:"allowance"`
}
// Task represents a task in the system.
type Task struct {
ID int `json:"id"`
Name string `json:"name"`
Reward int `json:"reward"`
Assigned *int `json:"assigned"` // Pointer to allow null
ID int `json:"id"`
Name string `json:"name"`
Reward float64 `json:"reward"`
Assigned *int `json:"assigned"` // Pointer to allow null
}
type Allowance struct {
ID int `json:"id"`
Name string `json:"name"`
Target int `json:"target"`
Progress int `json:"progress"`
Target float64 `json:"target"`
Progress float64 `json:"progress"`
Weight float64 `json:"weight"`
Colour string `json:"colour"`
}
type CreateAllowanceRequest struct {
Name string `json:"name"`
Target int `json:"target"`
Target float64 `json:"target"`
Weight float64 `json:"weight"`
Colour string `json:"colour"`
}
type UpdateAllowanceRequest struct {
Name string `json:"name"`
Target int `json:"target"`
Target float64 `json:"target"`
Weight float64 `json:"weight"`
Colour string `json:"colour"`
}
type BulkUpdateAllowanceRequest struct {
@@ -60,9 +63,9 @@ type CreateGoalResponse struct {
}
type CreateTaskRequest struct {
Name string `json:"name" binding:"required"`
Reward int `json:"reward"`
Assigned *int `json:"assigned"`
Name string `json:"name" binding:"required"`
Reward float64 `json:"reward"`
Assigned *int `json:"assigned"`
}
type CreateTaskResponse struct {

View File

@@ -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/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
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/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
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/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
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/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=

View File

@@ -587,9 +587,14 @@ func start(ctx context.Context, config *ServerConfig) {
defer db.db.MustClose()
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/user/:userId", getUser)
router.POST("/api/user/:userId/history", postHistory)

View File

@@ -0,0 +1,2 @@
alter table allowances
add column colour integer;

View File

@@ -0,0 +1,4 @@
alter table users rename column weight to weight_old;
alter table users add column weight real not null default 10.0;
update users set weight = weight_old;
alter table users drop column weight_old;

228
backend/web.go Normal file
View 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 &currentUser
}
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
View 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>

View File

@@ -11,6 +11,7 @@ const routes: Routes = [
path: '',
loadChildren: () => import('./pages/tabs/tabs.module').then(m => m.TabsPageModule)
},
];
@NgModule({
imports: [

View File

@@ -3,11 +3,12 @@ import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { Drivers, Storage } from '@ionic/storage';
import { Drivers } from '@ionic/storage';
import { IonicStorageModule } from '@ionic/storage-angular';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
declarations: [AppComponent],
@@ -15,6 +16,7 @@ import { AppComponent } from './app.component';
BrowserModule,
IonicModule.forRoot(),
AppRoutingModule,
ReactiveFormsModule,
IonicStorageModule.forRoot({
name: '__mydb',
driverOrder: [Drivers.IndexedDB, Drivers.LocalStorage]

View File

@@ -2,5 +2,5 @@ export interface Task {
id: number;
name: string;
reward: number;
assigned: number;
assigned: number | null;
}

View File

@@ -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 {}

View File

@@ -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 {}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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();
});
});

View File

@@ -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']);
}
}

View File

@@ -17,7 +17,7 @@ const routes: Routes = [
},
{
path: 'tasks',
loadChildren: () => import('../tasks/tasks.module').then(m => m.TasksPageModule)
loadChildren: () => import('../tasks/tasks.module').then(m => m.TasksPageModule),
},
{
path: '',

View File

@@ -6,7 +6,9 @@ const routes: Routes = [
{
path: '',
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({

View File

@@ -1,23 +1,30 @@
<ion-header [translucent]="true" class="ion-no-border">
<ion-toolbar>
<ion-title>
Tasks
</ion-title>
<div class="toolbar">
<ion-title>
Tasks
</ion-title>
<button class="add-button" (click)="createTask()">Add task</button>
</div>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="icon">
<mat-icon>filter_alt</mat-icon>
</div>
<div class="list">
<div class="task" *ngFor="let task of tasks">
<button>Done</button>
<div class="name">{{ task.name }}</div>
<div
class="reward"
[ngClass]="{ 'negative': task.reward < 0 }"
>{{ task.reward.toFixed(2) }} SP</div>
<div class="content">
<div class="icon">
<mat-icon>filter_alt</mat-icon>
</div>
<div class="list">
<div class="task" *ngFor="let task of tasks$ | async">
<button (click)="completeTask(task.id)">Done</button>
<div (click)="updateTask(task.id)" class="item">
<div class="name">{{ task.name }}</div>
<div
class="reward"
[ngClass]="{ 'negative': task.reward < 0 }"
>{{ task.reward.toFixed(2) }} SP</div>
</div>
</div>
</div>
</div>
</ion-content>

View File

@@ -1,3 +1,13 @@
.toolbar {
display: flex;
}
.content {
display: flex;
flex-direction: column;
height: 100%;
}
.icon {
padding: 5px;
display: flex;
@@ -23,6 +33,13 @@ mat-icon {
padding: 5px;
}
.item {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
}
.name {
margin-left: 10px;
color: var(--font-color);
@@ -44,4 +61,10 @@ button {
border-radius: 10px;
color: white;
background: var(--confirm-button-color);
}
.add-button {
background-color: var(--ion-color-primary);
margin-right: 15px;
width: 75px;
}

View File

@@ -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 { Task } from 'src/app/models/task';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject } from 'rxjs';
import { ViewWillEnter } from '@ionic/angular';
@Component({
selector: 'app-tasks',
templateUrl: 'tasks.page.html',
styleUrls: ['tasks.page.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class TasksPage implements OnInit {
public tasks: Array<Task> = [];
export class TasksPage implements ViewWillEnter {
public tasks$: BehaviorSubject<Array<Task>> = new BehaviorSubject<Array<Task>>([]);
constructor(
private taskService: TaskService
) {}
ngOnInit(): void {
this.taskService.getTaskList().subscribe(tasks => {
this.tasks = tasks;
});
private taskService: TaskService,
private router: Router,
private route: ActivatedRoute
) {
this.getTasks();
}
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();
}
}

View File

@@ -7,10 +7,31 @@ import { Task } from '../models/task';
providedIn: 'root'
})
export class TaskService {
private url = 'http://localhost:8080/api'
private url = 'http://localhost:8080/api';
constructor(private http: HttpClient) {}
getTaskList(): Observable<Array<Task>> {
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();
}
}