5 Commits

57 changed files with 103 additions and 1279 deletions

View File

@@ -1,9 +1,2 @@
# Allowance Planner 2000 # Allowance Planner 2000
An improved Allowance Planner app. An improved Allowance Planner app.
## Running backend
In order to run the backend, go to the `backend directory and run:
```bash
$ go run .
```

View File

@@ -52,9 +52,7 @@ func TestGetUserBadId(t *testing.T) {
func TestGetUserAllowanceWhenNoAllowancePresent(t *testing.T) { func TestGetUserAllowanceWhenNoAllowancePresent(t *testing.T) {
e := startServer(t) e := startServer(t)
result := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array() result := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
result.Length().IsEqual(1) result.Length().IsEqual(0)
item := result.Value(0).Object()
item.Value("id").IsEqual(0)
} }
func TestGetUserAllowance(t *testing.T) { func TestGetUserAllowance(t *testing.T) {
@@ -70,8 +68,8 @@ func TestGetUserAllowance(t *testing.T) {
// Validate allowance // Validate allowance
result := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array() result := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
result.Length().IsEqual(2) result.Length().IsEqual(1)
item := result.Value(1).Object() item := result.Value(0).Object()
item.Value("id").IsEqual(1) item.Value("id").IsEqual(1)
item.Value("name").IsEqual(TestAllowanceName) item.Value("name").IsEqual(TestAllowanceName)
item.Value("target").IsEqual(5000) item.Value("target").IsEqual(5000)
@@ -116,9 +114,9 @@ func TestCreateUserAllowance(t *testing.T) {
Status(200). Status(200).
JSON().Array() JSON().Array()
allowances.Length().IsEqual(2) allowances.Length().IsEqual(1)
allowance := allowances.Value(1).Object() allowance := allowances.Value(0).Object()
allowance.Value("id").IsEqual(allowanceId) allowance.Value("id").IsEqual(allowanceId)
allowance.Value("name").IsEqual(TestAllowanceName) allowance.Value("name").IsEqual(TestAllowanceName)
allowance.Value("target").IsEqual(5000) allowance.Value("target").IsEqual(5000)
@@ -210,12 +208,7 @@ func TestDeleteUserAllowance(t *testing.T) {
Expect(). Expect().
Status(200). Status(200).
JSON().Array() JSON().Array()
allowances.Length().IsEqual(1) allowances.Length().IsEqual(0)
}
func TestDeleteUserRestAllowance(t *testing.T) {
e := startServer(t)
e.DELETE("/user/1/allowance/0").Expect().Status(400)
} }
func TestDeleteUserAllowanceNotFound(t *testing.T) { func TestDeleteUserAllowanceNotFound(t *testing.T) {
@@ -255,16 +248,7 @@ func TestCreateTask(t *testing.T) {
// Verify the response has an ID // Verify the response has an ID
response.ContainsKey("id") response.ContainsKey("id")
response.Value("id").Number().IsEqual(1) taskId := response.Value("id").Number().Raw()
e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)
// Get task
result := e.GET("/task/1").Expect().Status(200).JSON().Object()
result.Value("id").IsEqual(1)
result.Value("name").IsEqual("Test Task")
result.Value("reward").IsEqual(100)
result.Value("assigned").IsNull()
// Create a new task with assigned user // Create a new task with assigned user
assignedUserId := 1 assignedUserId := 1
@@ -281,37 +265,7 @@ func TestCreateTask(t *testing.T) {
JSON().Object() JSON().Object()
responseWithUser.ContainsKey("id") responseWithUser.ContainsKey("id")
responseWithUser.Value("id").Number().IsEqual(2) responseWithUser.Value("id").Number().NotEqual(taskId) // Ensure different ID
}
func TestDeleteTask(t *testing.T) {
e := startServer(t)
// Create a new task without assigned user
requestBody := map[string]interface{}{
"name": "Test Task",
"reward": 100,
}
response := e.POST("/tasks").
WithJSON(requestBody).
Expect().
Status(201). // Expect Created status
JSON().Object()
// Verify the response has an ID
response.ContainsKey("id")
taskId := response.Value("id").Number().Raw()
// Delete the task
e.DELETE("/task/" + strconv.Itoa(int(taskId))).Expect().Status(200)
// Verify the task no longer exists
e.GET("/task/" + strconv.Itoa(int(taskId))).Expect().Status(404)
}
func TestDeleteTaskNotFound(t *testing.T) {
e := startServer(t)
e.DELETE("/task/1").Expect().Status(404)
} }
func TestCreateTaskNoName(t *testing.T) { func TestCreateTaskNoName(t *testing.T) {
@@ -361,12 +315,12 @@ func TestGetTaskWhenNoTasks(t *testing.T) {
result.Length().IsEqual(0) result.Length().IsEqual(0)
} }
func createTestTaskWithAmount(e *httpexpect.Expect, amount int) int { func createTestTask(e *httpexpect.Expect) {
requestBody := map[string]interface{}{ requestBody := map[string]interface{}{
"name": "Test Task", "name": "Test Task",
"reward": amount, "reward": 100,
} }
return int(e.POST("/tasks").WithJSON(requestBody).Expect().Status(201).JSON().Object().Value("id").Number().Raw()) e.POST("/tasks").WithJSON(requestBody).Expect().Status(201)
} }
func TestGetTasksWhenTasks(t *testing.T) { func TestGetTasksWhenTasks(t *testing.T) {
@@ -467,243 +421,8 @@ func TestGetHistory(t *testing.T) {
response.Value(2).Object().Value("allowance").Number().IsEqual(-10) response.Value(2).Object().Value("allowance").Number().IsEqual(-10)
} }
func TestGetUserAllowanceById(t *testing.T) {
e := startServer(t)
// Create a new allowance
requestBody := map[string]interface{}{
"name": TestAllowanceName,
"target": 5000,
"weight": 10,
}
resp := e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201).JSON().Object()
allowanceId := int(resp.Value("id").Number().Raw())
// 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("target").IsEqual(5000)
result.Value("weight").IsEqual(10)
result.Value("progress").IsEqual(0)
}
func TestGetUserByAllowanceIdInvalidAllowance(t *testing.T) {
e := startServer(t)
e.GET("/user/1/allowance/9999").Expect().Status(404)
}
func TestGetUserByAllowanceByIdInvalidUserId(t *testing.T) {
e := startServer(t)
e.GET("/user/999/allowance/1").Expect().Status(404)
}
func TestGetUserByAllowanceByIdBadUserId(t *testing.T) {
e := startServer(t)
e.GET("/user/bad/allowance/1").Expect().Status(400)
}
func TestGetUserByAllowanceByIdBadAllowanceId(t *testing.T) {
e := startServer(t)
e.GET("/user/1/allowance/bad").Expect().Status(400)
}
func TestPutAllowanceById(t *testing.T) {
e := startServer(t)
// Create a new allowance
requestBody := map[string]interface{}{
"name": TestAllowanceName,
"target": 5000,
"weight": 10,
}
resp := e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201).JSON().Object()
allowanceId := int(resp.Value("id").Number().Raw())
// Update the allowance
updateRequest := map[string]interface{}{
"name": "Updated Allowance",
"target": 6000,
"weight": 15,
}
e.PUT("/user/1/allowance/" + strconv.Itoa(allowanceId)).WithJSON(updateRequest).Expect().Status(200)
// Verify the allowance is updated
result := e.GET("/user/1/allowance/" + strconv.Itoa(allowanceId)).Expect().Status(200).JSON().Object()
result.Value("id").IsEqual(allowanceId)
result.Value("name").IsEqual("Updated Allowance")
result.Value("target").IsEqual(6000)
result.Value("weight").IsEqual(15)
}
func TestCompleteTask(t *testing.T) {
e := startServer(t)
taskId := createTestTaskWithAmount(e, 101)
e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)
// Update rest allowance
e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{
Weight: 25,
}).Expect().Status(200)
// Create two allowance goals
e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
Name: "Test Allowance 1",
Target: 1000,
Weight: 50,
}).Expect().Status(201)
e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
Name: "Test Allowance 1",
Target: 1000,
Weight: 25,
}).Expect().Status(201)
// Complete the task
e.POST("/task/" + strconv.Itoa(taskId) + "/complete").Expect().Status(200)
// Verify the task is marked as completed
e.GET("/task/" + strconv.Itoa(taskId)).Expect().Status(404)
// Verify the allowances are updated for user 1
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Length().IsEqual(3)
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
allowances.Value(0).Object().Value("progress").Number().IsEqual(26)
allowances.Value(1).Object().Value("id").Number().IsEqual(1)
allowances.Value(1).Object().Value("progress").Number().IsEqual(50)
allowances.Value(2).Object().Value("id").Number().IsEqual(2)
allowances.Value(2).Object().Value("progress").Number().IsEqual(25)
// 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().IsEqual(101)
for userId := 1; userId <= 2; userId++ {
userIdStr := strconv.Itoa(userId)
// Ensure the history got updated
history := e.GET("/user/" + userIdStr + "/history").Expect().Status(200).JSON().Array()
history.Length().IsEqual(1)
history.Value(0).Object().Value("allowance").Number().IsEqual(101)
history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
}
}
func TestCompleteTaskAllowanceWeightsSumTo0(t *testing.T) {
e := startServer(t)
taskId := createTestTaskWithAmount(e, 101)
e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)
// Update rest allowance
e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{
Weight: 0,
}).Expect().Status(200)
// Create an allowance goal
createTestAllowance(e, "Test Allowance 1", 1000, 0)
// Complete the task
e.POST("/task/" + strconv.Itoa(taskId) + "/complete").Expect().Status(200)
// Verify the task is marked as completed
e.GET("/task/" + strconv.Itoa(taskId)).Expect().Status(404)
// Verify the allowances are updated for user 1
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Length().IsEqual(2)
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
allowances.Value(0).Object().Value("progress").Number().IsEqual(101)
allowances.Value(1).Object().Value("id").Number().IsEqual(1)
allowances.Value(1).Object().Value("progress").Number().IsEqual(0)
}
func TestCompleteTaskInvalidId(t *testing.T) {
e := startServer(t)
e.POST("/task/999/complete").Expect().Status(404)
}
func TestCompleteAllowance(t *testing.T) {
e := startServer(t)
createTestTaskWithAmount(e, 100)
createTestAllowance(e, "Test Allowance 1", 100, 50)
// Complete the task
e.POST("/task/1/complete").Expect().Status(200)
// Complete allowance goal
e.POST("/user/1/allowance/1/complete").Expect().Status(200)
// Verify the allowance no longer exists
e.GET("/user/1/allowance/1").Expect().Status(404)
// Verify history is updated
history := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
history.Length().IsEqual(2)
history.Value(0).Object().Value("allowance").Number().IsEqual(100)
history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
history.Value(1).Object().Value("allowance").Number().IsEqual(-100)
history.Value(1).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
}
func TestCompleteAllowanceInvalidUserId(t *testing.T) {
e := startServer(t)
e.POST("/user/999/allowance/1/complete").Expect().Status(404)
}
func TestCompleteAllowanceInvalidAllowanceId(t *testing.T) {
e := startServer(t)
e.POST("/user/1/allowance/999/complete").Expect().Status(404)
}
func TestPutBulkAllowance(t *testing.T) {
e := startServer(t)
createTestAllowance(e, "Test Allowance 1", 1000, 1)
createTestAllowance(e, "Test Allowance 2", 1000, 2)
// Bulk edit
request := []map[string]interface{}{
{
"id": 1,
"weight": 5,
},
{
"id": 0,
"weight": 99,
},
{
"id": 2,
"weight": 10,
},
}
e.PUT("/user/1/allowance").WithJSON(request).Expect().Status(200)
// Verify the allowances are updated
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("weight").Number().IsEqual(99)
allowances.Value(1).Object().Value("id").Number().IsEqual(1)
allowances.Value(1).Object().Value("weight").Number().IsEqual(5)
allowances.Value(2).Object().Value("id").Number().IsEqual(2)
allowances.Value(2).Object().Value("weight").Number().IsEqual(10)
}
func getDelta(base time.Time, delta float64) (time.Time, time.Time) { func getDelta(base time.Time, delta float64) (time.Time, time.Time) {
start := base.Add(-time.Duration(delta) * time.Second) start := base.Add(-time.Duration(delta) * time.Second)
end := base.Add(time.Duration(delta) * time.Second) end := base.Add(time.Duration(delta) * time.Second)
return start, end return start, end
} }
func createTestAllowance(e *httpexpect.Expect, name string, target int, weight float64) {
e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
Name: name,
Target: target,
Weight: weight,
}).Expect().Status(201)
}
func createTestTask(e *httpexpect.Expect) int {
return createTestTaskWithAmount(e, 100)
}

View File

@@ -71,14 +71,7 @@ func (db *Db) GetUserAllowances(userId int) ([]Allowance, error) {
allowances := make([]Allowance, 0) allowances := make([]Allowance, 0)
var err error var err error
totalAllowance := Allowance{} for row := range db.db.Query("select id, name, target, progress, weight from allowances where user_id = ?").
err = db.db.Query("select balance, weight from users where id = ?").Bind(userId).ScanSingle(&totalAllowance.Progress, &totalAllowance.Weight)
if err != nil {
return nil, err
}
allowances = append(allowances, totalAllowance)
for row := range db.db.Query("select id, name, target, balance, weight 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) err = row.Scan(&allowance.ID, &allowance.Name, &allowance.Target, &allowance.Progress, &allowance.Weight)
@@ -93,25 +86,6 @@ func (db *Db) GetUserAllowances(userId int) ([]Allowance, error) {
return allowances, nil return allowances, nil
} }
func (db *Db) GetUserAllowanceById(userId int, allowanceId int) (*Allowance, error) {
allowance := &Allowance{}
if allowanceId == 0 {
err := db.db.Query("select balance, weight from users where id = ?").
Bind(userId).ScanSingle(&allowance.Progress, &allowance.Weight)
if err != nil {
return nil, err
}
} else {
err := db.db.Query("select id, name, target, balance, weight from allowances where user_id = ? and id = ?").
Bind(userId, allowanceId).
ScanSingle(&allowance.ID, &allowance.Name, &allowance.Target, &allowance.Progress, &allowance.Weight)
if err != nil {
return nil, err
}
}
return allowance, nil
}
func (db *Db) CreateAllowance(userId int, allowance *CreateAllowanceRequest) (int, error) { func (db *Db) CreateAllowance(userId int, allowance *CreateAllowanceRequest) (int, error) {
// Check if user exists before attempting to create an allowance // Check if user exists before attempting to create an allowance
exists, err := db.UserExists(userId) exists, err := db.UserExists(userId)
@@ -129,7 +103,7 @@ func (db *Db) CreateAllowance(userId int, allowance *CreateAllowanceRequest) (in
defer tx.MustRollback() defer tx.MustRollback()
// 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, progress, weight) values (?, ?, ?, 0, ?)").
Bind(userId, allowance.Name, allowance.Target, allowance.Weight). Bind(userId, allowance.Name, allowance.Target, allowance.Weight).
Exec() Exec()
@@ -175,107 +149,6 @@ func (db *Db) DeleteAllowance(userId int, allowanceId int) error {
return nil return nil
} }
func (db *Db) CompleteAllowance(userId int, allowanceId int) error {
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
// Get the cost of the allowance
var cost int
err = tx.Query("select balance from allowances where id = ? and user_id = ?").
Bind(allowanceId, userId).ScanSingle(&cost)
if err != nil {
return err
}
// Delete the allowance
err = tx.Query("delete from allowances where id = ? and user_id = ?").
Bind(allowanceId, userId).Exec()
if err != nil {
return err
}
// Add a history entry
err = tx.Query("insert into history (user_id, timestamp, amount) values (?, ?, ?)").
Bind(userId, time.Now().Unix(), -cost).
Exec()
if err != nil {
return err
}
return tx.Commit()
}
func (db *Db) UpdateUserAllowance(userId int, allowance *UpdateAllowanceRequest) error {
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
err = tx.Query("update users set weight=? where id = ?").
Bind(allowance.Weight, userId).
Exec()
if err != nil {
return err
}
return tx.Commit()
}
func (db *Db) UpdateAllowance(userId int, allowanceId int, allowance *UpdateAllowanceRequest) error {
// Check if the allowance exists for the user
count := 0
err := db.db.Query("select count(*) from allowances where id = ? and user_id = ?").
Bind(allowanceId, userId).ScanSingle(&count)
if err != nil {
return err
}
if count == 0 {
return errors.New("allowance not found")
}
tx, err := db.db.Begin()
if err != nil {
return err
}
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).
Exec()
if err != nil {
return err
}
return tx.Commit()
}
func (db *Db) BulkUpdateAllowance(userId int, allowances []BulkUpdateAllowanceRequest) error {
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
for _, allowance := range allowances {
if allowance.ID == 0 {
err = tx.Query("update users set weight=? where id = ?").
Bind(allowance.Weight, userId).
Exec()
} else {
err = tx.Query("update allowances set weight=? where id = ? and user_id = ?").
Bind(allowance.Weight, allowance.ID, userId).
Exec()
}
if err != nil {
return err
}
}
return tx.Commit()
}
func (db *Db) CreateTask(task *CreateTaskRequest) (int, error) { func (db *Db) CreateTask(task *CreateTaskRequest) (int, error) {
tx, err := db.db.Begin() tx, err := db.db.Begin()
if err != nil { if err != nil {
@@ -337,21 +210,6 @@ func (db *Db) GetTask(id int) (Task, error) {
return task, nil return task, nil
} }
func (db *Db) DeleteTask(id int) error {
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
err = tx.Query("delete from tasks where id = ?").Bind(id).Exec()
if err != nil {
return err
}
return tx.Commit()
}
func (db *Db) HasTask(id int) (bool, error) { func (db *Db) HasTask(id int) (bool, error) {
count := 0 count := 0
err := db.db.Query("select count(*) from tasks where id = ?"). err := db.db.Query("select count(*) from tasks where id = ?").
@@ -378,81 +236,6 @@ func (db *Db) UpdateTask(id int, task *CreateTaskRequest) error {
return tx.Commit() return tx.Commit()
} }
func (db *Db) CompleteTask(taskId int) error {
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
var reward int
err = tx.Query("select reward from tasks where id = ?").Bind(taskId).ScanSingle(&reward)
if err != nil {
return err
}
for userRow := range tx.Query("select id, weight from users").Range(&err) {
var userId int
var userWeight float64
err = userRow.Scan(&userId, &userWeight)
if err != nil {
return err
}
// Add the history entry
err = tx.Query("insert into history (user_id, timestamp, amount) values (?, ?, ?)").
Bind(userId, time.Now().Unix(), reward).
Exec()
if err != nil {
return err
}
// Calculate the sums of all weights
var sumOfWeights float64
err = tx.Query("select sum(weight) from allowances where user_id = ? and weight > 0").Bind(userId).ScanSingle(&sumOfWeights)
sumOfWeights += userWeight
remainingReward := reward
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
var allowanceWeight float64
err = allowanceRow.Scan(&allowanceId, &allowanceWeight)
if err != nil {
return err
}
// Calculate the amount to add to the allowance
amount := int((allowanceWeight / sumOfWeights) * float64(remainingReward))
sumOfWeights -= allowanceWeight
err = tx.Query("update allowances set balance = balance + ? where id = ? and user_id = ?").
Bind(amount, allowanceId, userId).Exec()
if err != nil {
return err
}
remainingReward -= amount
}
}
// Add the remaining reward to the user
err = tx.Query("update users set balance = balance + ? where id = ?").
Bind(remainingReward, userId).Exec()
if err != nil {
return err
}
}
if err != nil {
return err
}
// Remove the task
err = tx.Query("delete from tasks where id = ?").Bind(taskId).Exec()
return tx.Commit()
}
func (db *Db) AddHistory(userId int, allowance *PostHistory) error { func (db *Db) AddHistory(userId int, allowance *PostHistory) error {
tx, err := db.db.Begin() tx, err := db.db.Begin()
if err != nil { if err != nil {

View File

@@ -31,28 +31,17 @@ type Task struct {
} }
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 int `json:"target"`
Progress int `json:"progress"` Progress int `json:"progress"`
Weight float64 `json:"weight"` Weight int `json:"weight"`
} }
type CreateAllowanceRequest struct { type CreateAllowanceRequest struct {
Name string `json:"name"` Name string `json:"name"`
Target int `json:"target"` Target int `json:"target"`
Weight float64 `json:"weight"` Weight int `json:"weight"`
}
type UpdateAllowanceRequest struct {
Name string `json:"name"`
Target int `json:"target"`
Weight float64 `json:"weight"`
}
type BulkUpdateAllowanceRequest struct {
ID int `json:"id"`
Weight float64 `json:"weight"`
} }
type CreateGoalResponse struct { type CreateGoalResponse struct {

View File

@@ -3,7 +3,7 @@ module allowance_planner
go 1.24.2 go 1.24.2
require ( require (
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0 gitea.seeseepuff.be/seeseemelk/mysqlite v0.12.0
github.com/gavv/httpexpect/v2 v2.17.0 github.com/gavv/httpexpect/v2 v2.17.0
github.com/gin-contrib/cors v1.7.5 github.com/gin-contrib/cors v1.7.5
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0
@@ -68,9 +68,9 @@ require (
gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.65.7 // indirect modernc.org/libc v1.65.6 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.10.0 // indirect
modernc.org/sqlite v1.37.0 // indirect modernc.org/sqlite v1.37.0 // indirect
moul.io/http2curl/v2 v2.3.0 // indirect moul.io/http2curl/v2 v2.3.0 // indirect
zombiezen.com/go/sqlite v1.4.0 // indirect zombiezen.com/go/sqlite v1.4.0 // indirect

View File

@@ -1,9 +1,5 @@
gitea.seeseepuff.be/seeseemelk/mysqlite v0.12.0 h1:kl0VFgvm52UKxJhZpf1hvucxZdOoXY50g/VmzsWH+/8= 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.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= github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4=
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
@@ -220,14 +216,10 @@ 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 h1:OhJUhmuJ6MVZdqL5qmnd0/my46DKGFhSX4WOR7ijfyE=
modernc.org/libc v1.65.6/go.mod h1:MOiGAM9lrMBT9L8xT1nO41qYl5eg9gCp9/kWhz5L7WA= 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 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 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= 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= modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=

View File

@@ -106,49 +106,6 @@ func getUserAllowance(c *gin.Context) {
c.IndentedJSON(http.StatusOK, allowances) c.IndentedJSON(http.StatusOK, allowances)
} }
func getUserAllowanceById(c *gin.Context) {
userIdStr := c.Param("userId")
allowanceIdStr := c.Param("allowanceId")
userId, err := strconv.Atoi(userIdStr)
if err != nil {
log.Printf(ErrInvalidUserID+": %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID})
return
}
allowanceId, err := strconv.Atoi(allowanceIdStr)
if err != nil {
log.Printf("Invalid allowance ID: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid allowance ID"})
return
}
exists, err := db.UserExists(userId)
if err != nil {
log.Printf(ErrCheckingUserExist, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
if !exists {
c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound})
return
}
allowance, err := db.GetUserAllowanceById(userId, allowanceId)
if errors.Is(err, mysqlite.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "Allowance not found"})
return
}
if err != nil {
log.Printf("Error getting allowance: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
c.IndentedJSON(http.StatusOK, allowance)
}
func createUserAllowance(c *gin.Context) { func createUserAllowance(c *gin.Context) {
userIdStr := c.Param("userId") userIdStr := c.Param("userId")
userId, err := strconv.Atoi(userIdStr) userId, err := strconv.Atoi(userIdStr)
@@ -189,44 +146,6 @@ func createUserAllowance(c *gin.Context) {
c.IndentedJSON(http.StatusCreated, response) c.IndentedJSON(http.StatusCreated, response)
} }
func bulkPutUserAllowance(c *gin.Context) {
userIdStr := c.Param("userId")
userId, err := strconv.Atoi(userIdStr)
if err != nil {
log.Printf(ErrInvalidUserID+": %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID})
return
}
exists, err := db.UserExists(userId)
if err != nil {
log.Printf(ErrCheckingUserExist, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
if !exists {
c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound})
return
}
var allowanceRequest []BulkUpdateAllowanceRequest
if err := c.ShouldBindJSON(&allowanceRequest); err != nil {
log.Printf("Error parsing request body: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
err = db.BulkUpdateAllowance(userId, allowanceRequest)
if err != nil {
log.Printf("Error updating allowance: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
c.IndentedJSON(http.StatusOK, gin.H{"message": "Allowance updated successfully"})
}
func deleteUserAllowance(c *gin.Context) { func deleteUserAllowance(c *gin.Context) {
userIdStr := c.Param("userId") userIdStr := c.Param("userId")
allowanceIdStr := c.Param("allowanceId") allowanceIdStr := c.Param("allowanceId")
@@ -245,11 +164,6 @@ func deleteUserAllowance(c *gin.Context) {
return return
} }
if allowanceId == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Allowance id zero cannot be deleted"})
return
}
exists, err := db.UserExists(userId) exists, err := db.UserExists(userId)
if err != nil { if err != nil {
log.Printf(ErrCheckingUserExist, err) log.Printf(ErrCheckingUserExist, err)
@@ -275,99 +189,6 @@ func deleteUserAllowance(c *gin.Context) {
c.IndentedJSON(http.StatusOK, gin.H{"message": "History deleted successfully"}) c.IndentedJSON(http.StatusOK, gin.H{"message": "History deleted successfully"})
} }
func putUserAllowance(c *gin.Context) {
userIdStr := c.Param("userId")
allowanceIdStr := c.Param("allowanceId")
userId, err := strconv.Atoi(userIdStr)
if err != nil {
log.Printf(ErrInvalidUserID+": %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID})
return
}
allowanceId, err := strconv.Atoi(allowanceIdStr)
if err != nil {
log.Printf("Invalid allowance ID: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid allowance ID"})
return
}
exists, err := db.UserExists(userId)
if err != nil {
log.Printf(ErrCheckingUserExist, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
if !exists {
c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound})
return
}
var allowanceRequest UpdateAllowanceRequest
if err := c.ShouldBindJSON(&allowanceRequest); err != nil {
log.Printf("Error parsing request body: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
if allowanceId == 0 {
err = db.UpdateUserAllowance(userId, &allowanceRequest)
} else {
err = db.UpdateAllowance(userId, allowanceId, &allowanceRequest)
}
if err != nil {
log.Printf("Error updating allowance: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
c.IndentedJSON(http.StatusOK, gin.H{"message": "Allowance updated successfully"})
}
func completeAllowance(c *gin.Context) {
userIdStr := c.Param("userId")
allowanceIdStr := c.Param("allowanceId")
userId, err := strconv.Atoi(userIdStr)
if err != nil {
log.Printf(ErrInvalidUserID+": %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID})
return
}
allowanceId, err := strconv.Atoi(allowanceIdStr)
if err != nil {
log.Printf("Invalid allowance ID: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid allowance ID"})
return
}
exists, err := db.UserExists(userId)
if err != nil {
log.Printf(ErrCheckingUserExist, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
if !exists {
c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound})
return
}
err = db.CompleteAllowance(userId, allowanceId)
if errors.Is(err, mysqlite.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "Allowance not found"})
return
}
if err != nil {
log.Printf("Error completing allowance: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
c.IndentedJSON(http.StatusOK, gin.H{"message": "Allowance completed successfully"})
}
func createTask(c *gin.Context) { func createTask(c *gin.Context) {
var taskRequest CreateTaskRequest var taskRequest CreateTaskRequest
if err := c.ShouldBindJSON(&taskRequest); err != nil { if err := c.ShouldBindJSON(&taskRequest); err != nil {
@@ -469,60 +290,6 @@ func putTask(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Task updated successfully"}) c.JSON(http.StatusOK, gin.H{"message": "Task updated successfully"})
} }
func deleteTask(c *gin.Context) {
taskIdStr := c.Param("taskId")
taskId, err := strconv.Atoi(taskIdStr)
if err != nil {
log.Printf("Invalid task ID: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
return
}
hasTask, err := db.HasTask(taskId)
if err != nil {
log.Printf("Error checking task existence: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
if !hasTask {
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
return
}
err = db.DeleteTask(taskId)
if err != nil {
log.Printf("Error deleting task: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Task deleted successfully"})
}
func completeTask(c *gin.Context) {
taskIdStr := c.Param("taskId")
taskId, err := strconv.Atoi(taskIdStr)
if err != nil {
log.Printf("Invalid task ID: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
return
}
err = db.CompleteTask(taskId)
if errors.Is(err, mysqlite.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
return
}
if err != nil {
log.Printf("Error completing task: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Task completed successfully"})
}
func postHistory(c *gin.Context) { func postHistory(c *gin.Context) {
userIdStr := c.Param("userId") userIdStr := c.Param("userId")
userId, err := strconv.Atoi(userIdStr) userId, err := strconv.Atoi(userIdStr)
@@ -596,17 +363,11 @@ func start(ctx context.Context, config *ServerConfig) {
router.GET("/api/user/:userId/history", getHistory) router.GET("/api/user/:userId/history", getHistory)
router.GET("/api/user/:userId/allowance", getUserAllowance) router.GET("/api/user/:userId/allowance", getUserAllowance)
router.POST("/api/user/:userId/allowance", createUserAllowance) router.POST("/api/user/:userId/allowance", createUserAllowance)
router.PUT("/api/user/:userId/allowance", bulkPutUserAllowance)
router.GET("/api/user/:userId/allowance/:allowanceId", getUserAllowanceById)
router.DELETE("/api/user/:userId/allowance/:allowanceId", deleteUserAllowance) router.DELETE("/api/user/:userId/allowance/:allowanceId", deleteUserAllowance)
router.PUT("/api/user/:userId/allowance/:allowanceId", putUserAllowance)
router.POST("/api/user/:userId/allowance/:allowanceId/complete", completeAllowance)
router.POST("/api/tasks", createTask) router.POST("/api/tasks", createTask)
router.GET("/api/tasks", getTasks) router.GET("/api/tasks", getTasks)
router.GET("/api/task/:taskId", getTask) router.GET("/api/task/:taskId", getTask)
router.PUT("/api/task/:taskId", putTask) router.PUT("/api/task/:taskId", putTask)
router.DELETE("/api/task/:taskId", deleteTask)
router.POST("/api/task/:taskId/complete", completeTask)
srv := &http.Server{ srv := &http.Server{
Addr: config.Addr, Addr: config.Addr,
@@ -641,9 +402,5 @@ func main() {
Datasource: os.Getenv("DB_PATH"), Datasource: os.Getenv("DB_PATH"),
Addr: ":8080", Addr: ":8080",
} }
if config.Datasource == "" {
config.Datasource = "allowance_planner.db3"
log.Printf("Warning: No DB_PATH set, using default of %s", config.Datasource)
}
start(context.Background(), &config) start(context.Background(), &config)
} }

View File

@@ -1,9 +1,7 @@
create table users create table users
( (
id integer primary key, id integer primary key,
name text not null, name text not null
weight real not null default 0.0,
balance integer not null default 0
) strict; ) strict;
create table history create table history
@@ -20,7 +18,7 @@ create table allowances
user_id integer not null, user_id integer not null,
name text not null, name text not null,
target integer not null, target integer not null,
balance integer not null default 0, progress integer not null,
weight real not null weight real not null
); );

View File

@@ -25,7 +25,6 @@
"@capacitor/status-bar": "7.0.1", "@capacitor/status-bar": "7.0.1",
"@ionic/angular": "^8.0.0", "@ionic/angular": "^8.0.0",
"@ionic/pwa-elements": "^3.3.0", "@ionic/pwa-elements": "^3.3.0",
"@ionic/storage-angular": "^4.0.0",
"ionicons": "^7.0.0", "ionicons": "^7.0.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
@@ -3780,27 +3779,6 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
} }
}, },
"node_modules/@ionic/storage": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@ionic/storage/-/storage-4.0.0.tgz",
"integrity": "sha512-3N21P19Xk6cICLnSXZ3ilRqbSXAGSFeIF3HNqz+1kARcm0UFT/vwmZreaXtFyq437vvEWOfJ2enlj3JHLKS0FA==",
"dependencies": {
"localforage": "^1.9.0"
}
},
"node_modules/@ionic/storage-angular": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@ionic/storage-angular/-/storage-angular-4.0.0.tgz",
"integrity": "sha512-FeSmCMCm1bMRfu5TFSqLtjdfEo/dLLUhLIrPmbhSYomVZdV/dNn4mBZv9SabyxSqn4bF31hw40y+4buhG+durQ==",
"dependencies": {
"@ionic/storage": "^4.0.0",
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/core": "*",
"rxjs": "*"
}
},
"node_modules/@ionic/utils-array": { "node_modules/@ionic/utils-array": {
"version": "2.1.6", "version": "2.1.6",
"resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz", "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz",
@@ -10272,11 +10250,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"node_modules/immutable": { "node_modules/immutable": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
@@ -11818,14 +11791,6 @@
} }
} }
}, },
"node_modules/lie": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
"integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lines-and-columns": { "node_modules/lines-and-columns": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -11928,14 +11893,6 @@
"node": ">= 12.13.0" "node": ">= 12.13.0"
} }
}, },
"node_modules/localforage": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
"dependencies": {
"lie": "3.1.1"
}
},
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",

View File

@@ -30,7 +30,6 @@
"@capacitor/status-bar": "7.0.1", "@capacitor/status-bar": "7.0.1",
"@ionic/angular": "^8.0.0", "@ionic/angular": "^8.0.0",
"@ionic/pwa-elements": "^3.3.0", "@ionic/pwa-elements": "^3.3.0",
"@ionic/storage-angular": "^4.0.0",
"ionicons": "^7.0.0", "ionicons": "^7.0.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",

View File

@@ -4,7 +4,7 @@ import { AllowancePage } from './allowance.page';
const routes: Routes = [ const routes: Routes = [
{ {
path: ':id', path: '',
component: AllowancePage, component: AllowancePage,
} }
]; ];

View File

@@ -0,0 +1,15 @@
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title>
Allowance
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Allowance</ion-title>
</ion-toolbar>
</ion-header>
</ion-content>

View File

@@ -1,5 +1,4 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { UserService } from 'src/app/services/user.service';
@Component({ @Component({
selector: 'app-allowance', selector: 'app-allowance',
@@ -9,6 +8,6 @@ import { UserService } from 'src/app/services/user.service';
}) })
export class AllowancePage { export class AllowancePage {
constructor(private userService: UserService) {} constructor() {}
} }

View File

@@ -1,21 +1,15 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router'; import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
loadChildren: () => import('./pages/user-login/user-login.module').then( m => m.UserLoginPageModule) loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule)
}, }
{
path: '',
loadChildren: () => import('./pages/tabs/tabs.module').then(m => m.TabsPageModule)
},
]; ];
@NgModule({ @NgModule({
imports: [ imports: [
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }), RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
CommonModule
], ],
exports: [RouterModule] exports: [RouterModule]
}) })

View File

@@ -1,6 +1,4 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { StorageService } from './services/storage.service';
import { Router } from '@angular/router';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@@ -9,13 +7,5 @@ import { Router } from '@angular/router';
standalone: false, standalone: false,
}) })
export class AppComponent { export class AppComponent {
constructor(private storageService: StorageService, private router: Router) { constructor() {}
this.storageService.init().then(() => {
this.storageService.getCurrentUserId().then((userId) => {
if (userId !== undefined && userId !== null) {
this.router.navigate(['/tabs/allowance', userId]);
}
});
})
}
} }

View File

@@ -3,23 +3,13 @@ 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 { 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';
@NgModule({ @NgModule({
declarations: [AppComponent], declarations: [AppComponent],
imports: [ imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule],
BrowserModule,
IonicModule.forRoot(),
AppRoutingModule,
IonicStorageModule.forRoot({
name: '__mydb',
driverOrder: [Drivers.IndexedDB, Drivers.LocalStorage]
})
],
providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }], providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
bootstrap: [AppComponent], bootstrap: [AppComponent],
}) })

View File

@@ -0,0 +1,15 @@
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title>
History
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">History</ion-title>
</ion-toolbar>
</ion-header>
</ion-content>

View File

@@ -1,6 +0,0 @@
export interface Task {
id: number;
name: string;
reward: number;
assigned: number;
}

View File

@@ -1,5 +0,0 @@
export interface User {
id: number;
name: string;
allowance?: number
}

View File

@@ -1,10 +0,0 @@
<ion-header [translucent]="true" class="ion-no-border">
<ion-toolbar>
<ion-title>
Allowance
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
</ion-content>

View File

@@ -1,11 +0,0 @@
<ion-header [translucent]="true" class="ion-no-border">
<ion-toolbar>
<ion-title>
History
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
</ion-content>

View File

@@ -1,3 +0,0 @@
.tab-selected {
background-color: var(--ion-color-secondary);
}

View File

@@ -1,23 +0,0 @@
<ion-header [translucent]="true" class="ion-no-border">
<ion-toolbar>
<ion-title>
Tasks
</ion-title>
</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>
</div>
</ion-content>

View File

@@ -1,47 +0,0 @@
.icon {
padding: 5px;
display: flex;
justify-content: flex-end;
color: var(--ion-color-primary);
}
mat-icon {
font-size: 35px;
width: 35px;
height: 35px;
}
.list {
border-top: 1px solid var(--line-color);
}
.task {
display: flex;
flex-direction: row;
align-items: center;
border-bottom: 1px solid var(--line-color);
padding: 5px;
}
.name {
margin-left: 10px;
color: var(--font-color);
}
.reward {
margin-left: auto;
margin-right: 15px;
color: var(--positive-amount-color);
}
.negative {
color: var(--negative-amount-color);
}
button {
width: 57px;
height: 30px;
border-radius: 10px;
color: white;
background: var(--confirm-button-color);
}

View File

@@ -1,24 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { TaskService } from 'src/app/services/task.service';
import { Task } from 'src/app/models/task';
@Component({
selector: 'app-tasks',
templateUrl: 'tasks.page.html',
styleUrls: ['tasks.page.scss'],
standalone: false,
})
export class TasksPage implements OnInit {
public tasks: Array<Task> = [];
constructor(
private taskService: TaskService
) {}
ngOnInit(): void {
this.taskService.getTaskList().subscribe(tasks => {
this.tasks = tasks;
});
}
}

View File

@@ -1,17 +0,0 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { UserLoginPage } from './user-login.page';
const routes: Routes = [
{
path: '',
component: UserLoginPage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class UserLoginPageRoutingModule {}

View File

@@ -1,26 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { UserLoginPageRoutingModule } from './user-login-routing.module';
import { UserLoginPage } from './user-login.page';
import { provideHttpClient } from '@angular/common/http';
import { UserService } from 'src/app/services/user.service';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
UserLoginPageRoutingModule,
],
declarations: [UserLoginPage],
providers: [
provideHttpClient(),
UserService
]
})
export class UserLoginPageModule {}

View File

@@ -1,9 +0,0 @@
<ion-content>
<div class="title">Who are you?</div>
<div class="selection">
<div class="profile" *ngFor="let user of users">
<div class="picture" (click)="selectUser(user.id)"></div>
<div class="name">{{ user.name }}</div>
</div>
</div>
</ion-content>

View File

@@ -1,31 +0,0 @@
.title,
.selection,
.profile {
display: flex;
justify-content: center;
}
.title {
margin-top: 150px;
color: var(--ion-color-primary);
font-size: 40px;
}
.selection {
gap: 10%;
margin-top: 100px;
color: var(--ion-color-primary);
}
.profile {
flex-direction: column;
align-items: center;
}
.picture {
width: 130px;
height: 130px;
border: 1px solid var(--ion-color-primary);
border-radius: 10px;
background-color: var(--ion-color-secondary);
}

View File

@@ -1,17 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserLoginPage } from './user-login.page';
describe('UserLoginPage', () => {
let component: UserLoginPage;
let fixture: ComponentFixture<UserLoginPage>;
beforeEach(() => {
fixture = TestBed.createComponent(UserLoginPage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,32 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { User } from 'src/app/models/user';
import { StorageService } from 'src/app/services/storage.service';
import { UserService } from 'src/app/services/user.service';
@Component({
selector: 'app-user-login',
templateUrl: './user-login.page.html',
styleUrls: ['./user-login.page.scss'],
standalone: false,
})
export class UserLoginPage implements OnInit {
public users: Array<User> = [];
constructor(
private userService: UserService,
private storageService: StorageService,
private router: Router
) { }
ngOnInit() {
this.userService.getUserList().subscribe(users => {
this.users = users
});
}
selectUser(id: number) {
this.storageService.set('user-id', id);
this.router.navigate(['/tabs/allowance', id]);
}
}

View File

@@ -1,28 +0,0 @@
import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage-angular';
@Injectable({
providedIn: 'root'
})
export class StorageService {
private _storage: Storage | null = null;
constructor(private storage: Storage) {}
async init() {
// If using, define drivers here: await this.storage.defineDriver(/*...*/);
const storage = await this.storage.create();
this._storage = storage;
}
// Create and expose methods that users of this service can
// call, for example:
public set(key: string, value: any) {
this._storage?.set(key, value);
}
public async getCurrentUserId() {
return await this._storage?.get('user-id');
}
}

View File

@@ -1,16 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Task } from '../models/task';
@Injectable({
providedIn: 'root'
})
export class TaskService {
private url = 'http://localhost:8080/api'
constructor(private http: HttpClient) {}
getTaskList(): Observable<Array<Task>> {
return this.http.get<Task[]>(`${this.url}/tasks`);
}
}

View File

@@ -1,20 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { User } from '../models/user';
@Injectable({
providedIn: 'root',
})
export class UserService {
private url = 'http://localhost:8080/api';
constructor(private http: HttpClient) {}
getUserList(): Observable<Array<User>> {
return this.http.get<User[]>(`${this.url}/users`);
}
getUserById(id: number): Observable<User> {
return this.http.get<User>(`${this.url}/user/${id}`);
}
}

View File

@@ -34,6 +34,6 @@ const routes: Routes = [
]; ];
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes),], imports: [RouterModule.forChild(routes)],
}) })
export class TabsPageRoutingModule {} export class TabsPageRoutingModule {}

View File

@@ -7,8 +7,6 @@ import {MatIconModule} from '@angular/material/icon';
import { TabsPageRoutingModule } from './tabs-routing.module'; import { TabsPageRoutingModule } from './tabs-routing.module';
import { TabsPage } from './tabs.page'; import { TabsPage } from './tabs.page';
import { provideHttpClient } from '@angular/common/http';
import { UserService } from 'src/app/services/user.service';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -18,10 +16,6 @@ import { UserService } from 'src/app/services/user.service';
TabsPageRoutingModule, TabsPageRoutingModule,
MatIconModule, MatIconModule,
], ],
declarations: [TabsPage], declarations: [TabsPage]
providers: [
provideHttpClient(),
UserService
]
}) })
export class TabsPageModule {} export class TabsPageModule {}

View File

@@ -1,13 +1,17 @@
<ion-tabs> <ion-tabs>
<ion-tab-bar slot="bottom"> <ion-tab-bar slot="bottom">
<ion-tab-button tab="history" href="/tabs/history"> <ion-tab-button tab="history" href="/tabs/history">
<mat-icon>history</mat-icon> <mat-icon>history</mat-icon>
</ion-tab-button> </ion-tab-button>
<ion-tab-button tab="allowance" href="/tabs/allowance"> <ion-tab-button tab="allowance" href="/tabs/allowance">
<mat-icon>savings</mat-icon> <mat-icon>savings</mat-icon>
</ion-tab-button> </ion-tab-button>
<ion-tab-button tab="tasks" href="/tabs/tasks"> <ion-tab-button tab="tasks" href="/tabs/tasks">
<mat-icon>task_alt</mat-icon> <mat-icon>task_alt</mat-icon>
</ion-tab-button> </ion-tab-button>
</ion-tab-bar> </ion-tab-bar>
</ion-tabs> </ion-tabs>

View File

@@ -0,0 +1 @@

View File

@@ -7,6 +7,7 @@ import { Component } from '@angular/core';
standalone: false, standalone: false,
}) })
export class TabsPage { export class TabsPage {
constructor() {} constructor() {}
} }

View File

@@ -5,22 +5,14 @@ import { FormsModule } from '@angular/forms';
import { TasksPage } from './tasks.page'; import { TasksPage } from './tasks.page';
import { TasksPageRoutingModule } from './tasks-routing.module'; import { TasksPageRoutingModule } from './tasks-routing.module';
import { provideHttpClient } from '@angular/common/http';
import { TaskService } from 'src/app/services/task.service';
import { MatIconModule } from '@angular/material/icon';
@NgModule({ @NgModule({
imports: [ imports: [
IonicModule, IonicModule,
CommonModule, CommonModule,
FormsModule, FormsModule,
TasksPageRoutingModule, TasksPageRoutingModule
MatIconModule,
], ],
declarations: [TasksPage], declarations: [TasksPage]
providers: [
provideHttpClient(),
TaskService
]
}) })
export class TasksPageModule {} export class TasksPageModule {}

View File

@@ -0,0 +1,15 @@
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title>
Tasks
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Tasks</ion-title>
</ion-toolbar>
</ion-header>
</ion-content>

View File

@@ -0,0 +1,13 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-tasks',
templateUrl: 'tasks.page.html',
styleUrls: ['tasks.page.scss'],
standalone: false,
})
export class TasksPage {
constructor() {}
}

View File

@@ -1,45 +1 @@
/*
* App Global CSS
* ----------------------------------------------------------------------------
* Put style rules here that you want to apply globally. These styles are for
* the entire app and not just one component. Additionally, this file can be
* used as an entry point to import other CSS/Sass files to be included in the
* output CSS.
* For more information on global stylesheets, visit the documentation:
* https://ionicframework.com/docs/layout/global-stylesheets
*/
/* Core CSS required for Ionic components to work properly */
@import "@ionic/angular/css/core.css";
/* Basic CSS for apps built with Ionic */
@import "@ionic/angular/css/normalize.css";
@import "@ionic/angular/css/structure.css";
@import "@ionic/angular/css/typography.css";
@import "@ionic/angular/css/display.css";
/* Optional CSS utils that can be commented out */
@import "@ionic/angular/css/padding.css";
@import "@ionic/angular/css/float-elements.css";
@import "@ionic/angular/css/text-alignment.css";
@import "@ionic/angular/css/text-transformation.css";
@import "@ionic/angular/css/flex-utils.css";
/**
* Ionic Dark Mode
* -----------------------------------------------------
* For more info, please see:
* https://ionicframework.com/docs/theming/dark-mode
*/
/* @import "@ionic/angular/css/palettes/dark.always.css"; */
/* @import "@ionic/angular/css/palettes/dark.class.css"; */
@import "@ionic/angular/css/palettes/dark.system.css";
ion-title {
color: var(--ion-color-primary);
}
ion-header {
border-bottom: 1px solid var(--line-color);
}

View File

@@ -1,19 +1,2 @@
// For information on how to create your own theme, please see: // For information on how to create your own theme, please see:
// http://ionicframework.com/docs/theming/ // http://ionicframework.com/docs/theming/
:root {
--ion-color-primary: #9C4BE4;
--ion-color-secondary: #F5E9FF;
--ion-background-color: #F3F3F3;
--font-color: #7B7B7B;
--confirm-button-color: #58A66F;
--positive-amount-color: #7DCB7D;
--negative-amount-color: #C55454;
--line-color: #CACACA;
--ion-font-family: 'Myfont';
}
@font-face {
font-family: 'MyFont';
src: url('../assets/font/Jaro-Regular.ttf');
}