2 Commits

Author SHA1 Message Date
14910d8a5f Fix failing tests 2025-05-13 19:33:57 +02:00
01fb1a2b7d Do not catch norows error 2025-05-13 19:26:37 +02:00
79 changed files with 198 additions and 20475 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

@@ -5,11 +5,10 @@ import (
"github.com/gavv/httpexpect/v2" "github.com/gavv/httpexpect/v2"
"strconv" "strconv"
"testing" "testing"
"time"
) )
const ( const (
TestAllowanceName = "Test History" TestGoalName = "Test Goal"
) )
func startServer(t *testing.T) *httpexpect.Expect { func startServer(t *testing.T) *httpexpect.Expect {
@@ -36,7 +35,6 @@ func TestGetUser(t *testing.T) {
result := e.GET("/user/1").Expect().Status(200).JSON().Object() result := e.GET("/user/1").Expect().Status(200).JSON().Object()
result.Value("name").IsEqual("Seeseemelk") result.Value("name").IsEqual("Seeseemelk")
result.Value("id").IsEqual(1) result.Value("id").IsEqual(1)
result.Value("allowance").IsEqual(0)
} }
func TestGetUserUnknown(t *testing.T) { func TestGetUserUnknown(t *testing.T) {
@@ -49,58 +47,56 @@ func TestGetUserBadId(t *testing.T) {
e.GET("/user/bad-id").Expect().Status(400) e.GET("/user/bad-id").Expect().Status(400)
} }
func TestGetUserAllowanceWhenNoAllowancePresent(t *testing.T) { func TestGetUserGoalsWhenNoGoalsPresent(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/goals").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 TestGetUserGoals(t *testing.T) {
e := startServer(t) e := startServer(t)
// Create a new allowance // Create a new goal
requestBody := map[string]interface{}{ requestBody := map[string]interface{}{
"name": TestAllowanceName, "name": TestGoalName,
"target": 5000, "target": 5000,
"weight": 10, "weight": 10,
} }
e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201) e.POST("/user/1/goals").WithJSON(requestBody).Expect().Status(201)
// Validate allowance // Validate goal
result := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array() result := e.GET("/user/1/goals").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(TestGoalName)
item.Value("target").IsEqual(5000) item.Value("target").IsEqual(5000)
item.Value("weight").IsEqual(10) item.Value("weight").IsEqual(10)
item.Value("progress").IsEqual(0) item.Value("progress").IsEqual(0)
item.NotContainsKey("user_id") item.NotContainsKey("user_id")
} }
func TestGetUserAllowanceNoUser(t *testing.T) { func TestGetUserGoalsNoUser(t *testing.T) {
e := startServer(t) e := startServer(t)
e.GET("/user/999/allowance").Expect().Status(404) e.GET("/user/999/goals").Expect().Status(404)
} }
func TestGetUserAllowanceBadId(t *testing.T) { func TestGetUserGoalsBadId(t *testing.T) {
e := startServer(t) e := startServer(t)
e.GET("/user/bad-id/allowance").Expect().Status(400) e.GET("/user/bad-id/goals").Expect().Status(400)
} }
func TestCreateUserAllowance(t *testing.T) { func TestCreateUserGoal(t *testing.T) {
e := startServer(t) e := startServer(t)
// Create a new allowance // Create a new goal
requestBody := map[string]interface{}{ requestBody := map[string]interface{}{
"name": TestAllowanceName, "name": TestGoalName,
"target": 5000, "target": 5000,
"weight": 10, "weight": 10,
} }
response := e.POST("/user/1/allowance"). response := e.POST("/user/1/goals").
WithJSON(requestBody). WithJSON(requestBody).
Expect(). Expect().
Status(201). Status(201).
@@ -108,40 +104,40 @@ func TestCreateUserAllowance(t *testing.T) {
// Verify the response has an ID // Verify the response has an ID
response.ContainsKey("id") response.ContainsKey("id")
allowanceId := response.Value("id").Number().Raw() goalId := response.Value("id").Number().Raw()
// Verify the allowance exists in the list of allowances // Verify the goal exists in the list of goals
allowances := e.GET("/user/1/allowance"). goals := e.GET("/user/1/goals").
Expect(). Expect().
Status(200). Status(200).
JSON().Array() JSON().Array()
allowances.Length().IsEqual(2) goals.Length().IsEqual(1)
allowance := allowances.Value(1).Object() goal := goals.Value(0).Object()
allowance.Value("id").IsEqual(allowanceId) goal.Value("id").IsEqual(goalId)
allowance.Value("name").IsEqual(TestAllowanceName) goal.Value("name").IsEqual(TestGoalName)
allowance.Value("target").IsEqual(5000) goal.Value("target").IsEqual(5000)
allowance.Value("weight").IsEqual(10) goal.Value("weight").IsEqual(10)
allowance.Value("progress").IsEqual(0) goal.Value("progress").IsEqual(0)
} }
func TestCreateUserAllowanceNoUser(t *testing.T) { func TestCreateUserGoalNoUser(t *testing.T) {
e := startServer(t) e := startServer(t)
requestBody := map[string]interface{}{ requestBody := map[string]interface{}{
"name": TestAllowanceName, "name": TestGoalName,
"target": 5000, "target": 5000,
"weight": 10, "weight": 10,
} }
e.POST("/user/999/allowance"). e.POST("/user/999/goals").
WithJSON(requestBody). WithJSON(requestBody).
Expect(). Expect().
Status(404) Status(404)
} }
func TestCreateUserAllowanceInvalidInput(t *testing.T) { func TestCreateUserGoalInvalidInput(t *testing.T) {
e := startServer(t) e := startServer(t)
// Test with empty name // Test with empty name
@@ -151,7 +147,7 @@ func TestCreateUserAllowanceInvalidInput(t *testing.T) {
"weight": 10, "weight": 10,
} }
e.POST("/user/1/allowance"). e.POST("/user/1/goals").
WithJSON(requestBody). WithJSON(requestBody).
Expect(). Expect().
Status(400) Status(400)
@@ -161,81 +157,76 @@ func TestCreateUserAllowanceInvalidInput(t *testing.T) {
"target": 5000, "target": 5000,
} }
e.POST("/user/1/allowance"). e.POST("/user/1/goals").
WithJSON(invalidRequest). WithJSON(invalidRequest).
Expect(). Expect().
Status(400) Status(400)
} }
func TestCreateUserAllowanceBadId(t *testing.T) { func TestCreateUserGoalBadId(t *testing.T) {
e := startServer(t) e := startServer(t)
requestBody := map[string]interface{}{ requestBody := map[string]interface{}{
"name": TestAllowanceName, "name": TestGoalName,
"target": 5000, "target": 5000,
"weight": 10, "weight": 10,
} }
e.POST("/user/bad-id/allowance"). e.POST("/user/bad-id/goals").
WithJSON(requestBody). WithJSON(requestBody).
Expect(). Expect().
Status(400) Status(400)
} }
func TestDeleteUserAllowance(t *testing.T) { func TestDeleteUserGoal(t *testing.T) {
e := startServer(t) e := startServer(t)
// Create a new allowance to delete // Create a new goal to delete
createRequest := map[string]interface{}{ createRequest := map[string]interface{}{
"name": TestAllowanceName, "name": TestGoalName,
"target": 1000, "target": 1000,
"weight": 5, "weight": 5,
} }
response := e.POST("/user/1/allowance"). response := e.POST("/user/1/goals").
WithJSON(createRequest). WithJSON(createRequest).
Expect(). Expect().
Status(201). Status(201).
JSON().Object() JSON().Object()
allowanceId := response.Value("id").Number().Raw() goalId := response.Value("id").Number().Raw()
// Delete the allowance // Delete the goal
e.DELETE("/user/1/allowance/" + strconv.Itoa(int(allowanceId))). e.DELETE("/user/1/goal/" + strconv.Itoa(int(goalId))).
Expect(). Expect().
Status(200). Status(200).
JSON().Object().Value("message").IsEqual("History deleted successfully") JSON().Object().Value("message").IsEqual("Goal deleted successfully")
// Verify the allowance no longer exists // Verify the goal no longer exists
allowances := e.GET("/user/1/allowance"). goals := e.GET("/user/1/goals").
Expect(). Expect().
Status(200). Status(200).
JSON().Array() JSON().Array()
allowances.Length().IsEqual(1) goals.Length().IsEqual(0)
} }
func TestDeleteUserRestAllowance(t *testing.T) { func TestDeleteUserGoalNotFound(t *testing.T) {
e := startServer(t)
e.DELETE("/user/1/allowance/0").Expect().Status(400)
}
func TestDeleteUserAllowanceNotFound(t *testing.T) {
e := startServer(t) e := startServer(t)
// Attempt to delete a non-existent allowance // Attempt to delete a non-existent goal
e.DELETE("/user/1/allowance/999"). e.DELETE("/user/1/goal/999").
Expect(). Expect().
Status(404). Status(404).
JSON().Object().Value("error").IsEqual("History not found") JSON().Object().Value("error").IsEqual("Goal not found")
} }
func TestDeleteUserAllowanceInvalidId(t *testing.T) { func TestDeleteUserGoalInvalidId(t *testing.T) {
e := startServer(t) e := startServer(t)
// Attempt to delete an allowance with an invalid ID // Attempt to delete a goal with an invalid ID
e.DELETE("/user/1/allowance/invalid-id"). e.DELETE("/user/1/goal/invalid-id").
Expect(). Expect().
Status(400). Status(400).
JSON().Object().Value("error").IsEqual("Invalid allowance ID") JSON().Object().Value("error").IsEqual("Invalid goal ID")
} }
func TestCreateTask(t *testing.T) { func TestCreateTask(t *testing.T) {
@@ -255,16 +246,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 +263,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,15 +313,15 @@ 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) {
e := startServer(t) e := startServer(t)
createTestTask(e) createTestTask(e)
@@ -437,9 +389,9 @@ func TestPutTaskInvalidTaskId(t *testing.T) {
func TestPostAllowance(t *testing.T) { func TestPostAllowance(t *testing.T) {
e := startServer(t) e := startServer(t)
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 100}).Expect().Status(200) e.POST("/user/1/allowance").WithJSON(PostAllowance{Allowance: 100}).Expect().Status(200)
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 20}).Expect().Status(200) e.POST("/user/1/allowance").WithJSON(PostAllowance{Allowance: 20}).Expect().Status(200)
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: -10}).Expect().Status(200) e.POST("/user/1/allowance").WithJSON(PostAllowance{Allowance: -10}).Expect().Status(200)
response := e.GET("/user/1").Expect().Status(200).JSON().Object() response := e.GET("/user/1").Expect().Status(200).JSON().Object()
response.Value("allowance").Number().IsEqual(100 + 20 - 10) response.Value("allowance").Number().IsEqual(100 + 20 - 10)
@@ -448,262 +400,7 @@ func TestPostAllowance(t *testing.T) {
func TestPostAllowanceInvalidUserId(t *testing.T) { func TestPostAllowanceInvalidUserId(t *testing.T) {
e := startServer(t) e := startServer(t)
e.POST("/user/999/history").WithJSON(PostHistory{Allowance: 100}).Expect(). e.POST("/user/999/allowance").WithJSON(PostAllowance{Allowance: 100}).Expect().
Status(404) Status(404)
}
func TestGetHistory(t *testing.T) {
e := startServer(t)
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 100}).Expect().Status(200)
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 20}).Expect().Status(200)
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: -10}).Expect().Status(200)
response := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
response.Length().IsEqual(3)
response.Value(0).Object().Value("allowance").Number().IsEqual(100)
response.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
response.Value(1).Object().Value("allowance").Number().IsEqual(20)
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) {
start := base.Add(-time.Duration(delta) * time.Second)
end := base.Add(time.Duration(delta) * time.Second)
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

@@ -67,53 +67,27 @@ func (db *Db) UserExists(userId int) (bool, error) {
return count > 0, nil return count > 0, nil
} }
func (db *Db) GetUserAllowances(userId int) ([]Allowance, error) { func (db *Db) GetUserGoals(userId int) ([]Goal, error) {
allowances := make([]Allowance, 0) goals := make([]Goal, 0)
var err error var err error
totalAllowance := Allowance{} for row := range db.db.Query("select id, name, target, progress, weight from goals 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{} goal := Goal{}
err = row.Scan(&allowance.ID, &allowance.Name, &allowance.Target, &allowance.Progress, &allowance.Weight) err = row.Scan(&goal.ID, &goal.Name, &goal.Target, &goal.Progress, &goal.Weight)
if err != nil { if err != nil {
return nil, err return nil, err
} }
allowances = append(allowances, allowance) goals = append(goals, goal)
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
return allowances, nil return goals, nil
} }
func (db *Db) GetUserAllowanceById(userId int, allowanceId int) (*Allowance, error) { func (db *Db) CreateGoal(userId int, goal *CreateGoalRequest) (int, error) {
allowance := &Allowance{} // Check if user exists before attempting to create a goal
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) {
// Check if user exists before attempting to create an allowance
exists, err := db.UserExists(userId) exists, err := db.UserExists(userId)
if err != nil { if err != nil {
return 0, err return 0, err
@@ -128,9 +102,9 @@ func (db *Db) CreateAllowance(userId int, allowance *CreateAllowanceRequest) (in
} }
defer tx.MustRollback() defer tx.MustRollback()
// Insert the new allowance // Insert the new goal
err = tx.Query("insert into allowances (user_id, name, target, weight) values (?, ?, ?, ?)"). err = tx.Query("insert into goals (user_id, name, target, progress, weight) values (?, ?, ?, 0, ?)").
Bind(userId, allowance.Name, allowance.Target, allowance.Weight). Bind(userId, goal.Name, goal.Target, goal.Weight).
Exec() Exec()
if err != nil { if err != nil {
@@ -153,21 +127,21 @@ func (db *Db) CreateAllowance(userId int, allowance *CreateAllowanceRequest) (in
return lastId, nil return lastId, nil
} }
func (db *Db) DeleteAllowance(userId int, allowanceId int) error { func (db *Db) DeleteGoal(userId int, goalId int) error {
// Check if the allowance exists for the user // Check if the goal exists for the user
count := 0 count := 0
err := db.db.Query("select count(*) from allowances where id = ? and user_id = ?"). err := db.db.Query("select count(*) from goals where id = ? and user_id = ?").
Bind(allowanceId, userId).ScanSingle(&count) Bind(goalId, userId).ScanSingle(&count)
if err != nil { if err != nil {
return err return err
} }
if count == 0 { if count == 0 {
return errors.New("allowance not found") return errors.New("goal not found")
} }
// Delete the allowance // Delete the goal
err = db.db.Query("delete from allowances where id = ? and user_id = ?"). err = db.db.Query("delete from goals where id = ? and user_id = ?").
Bind(allowanceId, userId).Exec() Bind(goalId, userId).Exec()
if err != nil { if err != nil {
return err return err
} }
@@ -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,89 +236,14 @@ func (db *Db) UpdateTask(id int, task *CreateTaskRequest) error {
return tx.Commit() return tx.Commit()
} }
func (db *Db) CompleteTask(taskId int) error { func (db *Db) AddAllowance(userId int, allowance *PostAllowance) error {
tx, err := db.db.Begin() tx, err := db.db.Begin()
if err != nil { if err != nil {
return err return err
} }
defer tx.MustRollback() defer tx.MustRollback()
var reward int err = tx.Query("insert into history (user_id, date, amount) values (?, ?, ?)").
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 {
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
err = tx.Query("insert into history (user_id, timestamp, amount) values (?, ?, ?)").
Bind(userId, time.Now().Unix(), allowance.Allowance). Bind(userId, time.Now().Unix(), allowance.Allowance).
Exec() Exec()
if err != nil { if err != nil {
@@ -468,24 +251,3 @@ func (db *Db) AddHistory(userId int, allowance *PostHistory) error {
} }
return tx.Commit() return tx.Commit()
} }
func (db *Db) GetHistory(userId int) ([]History, error) {
history := make([]History, 0)
var err 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)
if err != nil {
return nil, err
}
allowance.Timestamp = time.Unix(timestamp, 0)
history = append(history, allowance)
}
if err != nil {
return nil, err
}
return history, nil
}

View File

@@ -1,7 +1,5 @@
package main package main
import "time"
type User struct { type User struct {
ID int `json:"id"` ID int `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -13,12 +11,12 @@ type UserWithAllowance struct {
Allowance int `json:"allowance"` Allowance int `json:"allowance"`
} }
type History struct { type Allowance struct {
Allowance int `json:"allowance"` Allowance int `json:"allowance"`
Timestamp time.Time `json:"timestamp"` Goals []Goal `json:"goals"`
} }
type PostHistory struct { type PostAllowance struct {
Allowance int `json:"allowance"` Allowance int `json:"allowance"`
} }
@@ -30,29 +28,18 @@ type Task struct {
Assigned *int `json:"assigned"` // Pointer to allow null Assigned *int `json:"assigned"` // Pointer to allow null
} }
type Allowance struct { type Goal 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 CreateGoalRequest 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,9 +3,8 @@ 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.11.1
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-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0
) )
@@ -68,9 +67,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.2 // 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.11.1 h1:5s0r2IRpomGJC6pjirdMk7HAcAYEydLK5AhBZy+V1Ys=
gitea.seeseepuff.be/seeseemelk/mysqlite v0.12.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE= gitea.seeseepuff.be/seeseemelk/mysqlite v0.11.1/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=
@@ -32,8 +28,6 @@ github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBv
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gavv/httpexpect/v2 v2.17.0 h1:nIJqt5v5e4P7/0jODpX2gtSw+pHXUqdP28YcjqwDZmE= github.com/gavv/httpexpect/v2 v2.17.0 h1:nIJqt5v5e4P7/0jODpX2gtSw+pHXUqdP28YcjqwDZmE=
github.com/gavv/httpexpect/v2 v2.17.0/go.mod h1:E8ENFlT9MZ3Si2sfM6c6ONdwXV2noBCGkhA+lkJgkP0= github.com/gavv/httpexpect/v2 v2.17.0/go.mod h1:E8ENFlT9MZ3Si2sfM6c6ONdwXV2noBCGkhA+lkJgkP0=
github.com/gin-contrib/cors v1.7.5 h1:cXC9SmofOrRg0w9PigwGlHG3ztswH6bqq4vJVXnvYMk=
github.com/gin-contrib/cors v1.7.5/go.mod h1:4q3yi7xBEDDWKapjT2o1V7mScKDDr8k+jZ0fSquGoy0=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
@@ -74,9 +68,8 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
@@ -107,8 +100,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg= github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg=
github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
@@ -197,9 +188,8 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
@@ -212,22 +202,18 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s= modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= modernc.org/ccgo/v4 v4.27.1 h1:emhLB4uoOmkZUnTDFcMI3AbkmU/Evjuerit9Taqe6Ss=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= modernc.org/ccgo/v4 v4.27.1/go.mod h1:543Q0qQhJWekKVS5P6yL5fO6liNhla9Lbm2/B3rEKDE=
modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8= modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8=
modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.65.6 h1:OhJUhmuJ6MVZdqL5qmnd0/my46DKGFhSX4WOR7ijfyE= modernc.org/libc v1.65.2 h1:drWL1QO9fKXr3kXDN8y+4lKyBr8bA3mtUBQpftq3IJw=
modernc.org/libc v1.65.6/go.mod h1:MOiGAM9lrMBT9L8xT1nO41qYl5eg9gCp9/kWhz5L7WA= modernc.org/libc v1.65.2/go.mod h1:VI3V2S5mNka4deJErQ0jsMXe7jgxojE2fOB/mWoHlbc=
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

@@ -11,7 +11,6 @@ import (
"os" "os"
"strconv" "strconv"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -76,7 +75,7 @@ func getUser(c *gin.Context) {
c.IndentedJSON(http.StatusOK, user) c.IndentedJSON(http.StatusOK, user)
} }
func getUserAllowance(c *gin.Context) { func getUserGoals(c *gin.Context) {
userIdStr := c.Param("userId") userIdStr := c.Param("userId")
userId, err := strconv.Atoi(userIdStr) userId, err := strconv.Atoi(userIdStr)
if err != nil { if err != nil {
@@ -97,59 +96,16 @@ func getUserAllowance(c *gin.Context) {
return return
} }
allowances, err := db.GetUserAllowances(userId) goals, err := db.GetUserGoals(userId)
if err != nil { if err != nil {
log.Printf("Error getting user allowance: %v", err) log.Printf("Error getting user goals: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return return
} }
c.IndentedJSON(http.StatusOK, allowances) c.IndentedJSON(http.StatusOK, goals)
} }
func getUserAllowanceById(c *gin.Context) { func createUserGoal(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) {
userIdStr := c.Param("userId") userIdStr := c.Param("userId")
userId, err := strconv.Atoi(userIdStr) userId, err := strconv.Atoi(userIdStr)
if err != nil { if err != nil {
@@ -159,7 +115,7 @@ func createUserAllowance(c *gin.Context) {
} }
// Parse request body // Parse request body
var goalRequest CreateAllowanceRequest var goalRequest CreateGoalRequest
if err := c.ShouldBindJSON(&goalRequest); err != nil { if err := c.ShouldBindJSON(&goalRequest); err != nil {
log.Printf("Error parsing request body: %v", err) log.Printf("Error parsing request body: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
@@ -168,12 +124,12 @@ func createUserAllowance(c *gin.Context) {
// Validate request // Validate request
if goalRequest.Name == "" { if goalRequest.Name == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Allowance name cannot be empty"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Goal name cannot be empty"})
return return
} }
// Create goal in database // Create goal in database
goalId, err := db.CreateAllowance(userId, &goalRequest) goalId, err := db.CreateGoal(userId, &goalRequest)
if err != nil { if err != nil {
log.Printf("Error creating goal: %v", err) log.Printf("Error creating goal: %v", err)
if err.Error() == "user does not exist" { if err.Error() == "user does not exist" {
@@ -189,8 +145,9 @@ func createUserAllowance(c *gin.Context) {
c.IndentedJSON(http.StatusCreated, response) c.IndentedJSON(http.StatusCreated, response)
} }
func bulkPutUserAllowance(c *gin.Context) { func deleteUserGoal(c *gin.Context) {
userIdStr := c.Param("userId") userIdStr := c.Param("userId")
goalIdStr := c.Param("goalId")
userId, err := strconv.Atoi(userIdStr) userId, err := strconv.Atoi(userIdStr)
if err != nil { if err != nil {
@@ -199,6 +156,13 @@ func bulkPutUserAllowance(c *gin.Context) {
return return
} }
goalId, err := strconv.Atoi(goalIdStr)
if err != nil {
log.Printf("Invalid goal ID: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid goal ID"})
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)
@@ -210,162 +174,18 @@ func bulkPutUserAllowance(c *gin.Context) {
return return
} }
var allowanceRequest []BulkUpdateAllowanceRequest err = db.DeleteGoal(userId, goalId)
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 { if err != nil {
log.Printf("Error updating allowance: %v", err) if err.Error() == "goal not found" {
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"})
return
}
c.IndentedJSON(http.StatusOK, gin.H{"message": "Allowance updated successfully"})
}
func deleteUserAllowance(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
}
if allowanceId == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Allowance id zero cannot be deleted"})
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.DeleteAllowance(userId, allowanceId)
if err != nil {
if err.Error() == "allowance not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "History not found"})
} else { } else {
log.Printf("Error deleting allowance: %v", err) log.Printf("Error deleting goal: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
} }
return return
} }
c.IndentedJSON(http.StatusOK, gin.H{"message": "History deleted successfully"}) c.JSON(http.StatusOK, gin.H{"message": "Goal 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) {
@@ -413,7 +233,7 @@ func getTasks(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return return
} }
c.IndentedJSON(http.StatusOK, &response) c.JSON(http.StatusOK, &response)
} }
func getTask(c *gin.Context) { func getTask(c *gin.Context) {
@@ -469,61 +289,7 @@ 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) { func postAllowance(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) {
userIdStr := c.Param("userId") userIdStr := c.Param("userId")
userId, err := strconv.Atoi(userIdStr) userId, err := strconv.Atoi(userIdStr)
if err != nil { if err != nil {
@@ -532,8 +298,8 @@ func postHistory(c *gin.Context) {
return return
} }
var historyRequest PostHistory var allowanceRequest PostAllowance
if err := c.ShouldBindJSON(&historyRequest); err != nil { if err := c.ShouldBindJSON(&allowanceRequest); err != nil {
log.Printf("Error parsing request body: %v", err) log.Printf("Error parsing request body: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return return
@@ -550,35 +316,17 @@ func postHistory(c *gin.Context) {
return return
} }
err = db.AddHistory(userId, &historyRequest) err = db.AddAllowance(userId, &allowanceRequest)
if err != nil { if err != nil {
log.Printf("Error updating history: %v", err) log.Printf("Error updating allowance: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return return
} }
c.JSON(http.StatusOK, gin.H{"message": "History updated successfully"}) c.JSON(http.StatusOK, gin.H{"message": "Allowance updated successfully"})
}
func getHistory(c *gin.Context) {
userIdStr := c.Param("userId")
userId, err := strconv.Atoi(userIdStr)
if err != nil {
log.Printf("Invalid user ID: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
history, err := db.GetHistory(userId)
if err != nil {
log.Printf("Error getting history: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
c.IndentedJSON(http.StatusOK, history)
} }
/* /*
*
Initialises the database, and then starts the server. Initialises the database, and then starts the server.
If the context gets cancelled, the server is shutdown and the database is closed. If the context gets cancelled, the server is shutdown and the database is closed.
*/ */
@@ -587,26 +335,16 @@ func start(ctx context.Context, config *ServerConfig) {
defer db.db.MustClose() defer db.db.MustClose()
router := gin.Default() router := gin.Default()
router.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"},
}))
router.GET("/api/users", getUsers) router.GET("/api/users", getUsers)
router.GET("/api/user/:userId", getUser) router.GET("/api/user/:userId", getUser)
router.POST("/api/user/:userId/history", postHistory) router.GET("/api/user/:userId/goals", getUserGoals)
router.GET("/api/user/:userId/history", getHistory) router.POST("/api/user/:userId/goals", createUserGoal)
router.GET("/api/user/:userId/allowance", getUserAllowance) router.DELETE("/api/user/:userId/goal/:goalId", deleteUserGoal)
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.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/user/:userId/allowance", postAllowance)
router.POST("/api/task/:taskId/complete", completeTask)
srv := &http.Server{ srv := &http.Server{
Addr: config.Addr, Addr: config.Addr,
@@ -639,11 +377,6 @@ func start(ctx context.Context, config *ServerConfig) {
func main() { func main() {
config := ServerConfig{ config := ServerConfig{
Datasource: os.Getenv("DB_PATH"), Datasource: os.Getenv("DB_PATH"),
Addr: ":8080",
}
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,26 +1,24 @@
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
( (
id integer primary key, id integer primary key,
user_id integer not null, user_id integer not null,
timestamp date not null, date date not null,
amount integer not null amount integer not null
); );
create table allowances create table goals
( (
id integer primary key, id integer primary key,
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

@@ -59,9 +59,9 @@ paths:
404: 404:
description: The users could not be found. description: The users could not be found.
/user/{userId}/history: /user/{userId}/allowance:
get: get:
summary: Gets the allowance history of a user summary: Gets the allowance breakdown of a user
parameters: parameters:
- in: path - in: path
name: userId name: userId
@@ -71,21 +71,18 @@ paths:
type: integer type: integer
responses: responses:
200: 200:
description: Information about the allowance history of the user description: Information about the allowance of the user
content: content:
application/json: application/json:
schema: schema:
type: array
items:
type: object type: object
properties: properties:
date: allowance:
type: string
format: date-time
description: The date of the allowance or expense.
amount:
type: integer type: integer
description: The amount of the allowance to be added, in cents. A negative value description: The total allowance value of the user, in cents.
goals:
type: array
$ref: "#/components/schemas/goal"
post: post:
summary: Updates the allowance of a user summary: Updates the allowance of a user
parameters: parameters:
@@ -114,7 +111,35 @@ paths:
400: 400:
description: The allowance could not be updated. description: The allowance could not be updated.
/user/{userId}/allowance: /user/{userId}/history:
get:
summary: Gets the allowance history of a user
parameters:
- in: path
name: userId
description: The user ID
required: true
schema:
type: integer
responses:
200:
description: Information about the allowance history of the user
content:
application/json:
schema:
type: array
items:
type: object
properties:
date:
type: string
format: date-time
description: The date of the allowance or expense.
amount:
type: integer
description: The amount of the allowance to be added, in cents. A negative value
/user/{userId}/goals:
get: get:
summary: Gets all goals summary: Gets all goals
parameters: parameters:
@@ -201,7 +226,7 @@ paths:
404: 404:
description: The goals could not be found. description: The goals could not be found.
/user/{userId}/allowance/{goalId}: /user/{userId}/goal/{goalId}:
get: get:
summary: Gets information about a goal summary: Gets information about a goal
parameters: parameters:
@@ -284,7 +309,7 @@ paths:
404: 404:
description: The goal could not be found. description: The goal could not be found.
/user/{userId}/allowance/{goalId}/complete: /user/{userId}/goal/{goalId}/complete:
post: post:
summary: Completes a goal. summary: Completes a goal.
description: Completes a goal. This will subtract this goal's value from the user's allowance and then remove the goal. description: Completes a goal. This will subtract this goal's value from the user's allowance and then remove the goal.

View File

@@ -1,16 +0,0 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
Chrome >=79
ChromeAndroid >=79
Firefox >=70
Edge >=79
Safari >=14
iOS >=14

View File

@@ -1,16 +0,0 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

View File

@@ -1,47 +0,0 @@
{
"root": true,
"ignorePatterns": ["projects/**/*"],
"overrides": [
{
"files": ["*.ts"],
"parserOptions": {
"project": ["tsconfig.json"],
"createDefaultProgram": true
},
"extends": [
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/prefer-standalone": "off",
"@angular-eslint/component-class-suffix": [
"error",
{
"suffixes": ["Page", "Component"]
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
],
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
]
}
},
{
"files": ["*.html"],
"extends": ["plugin:@angular-eslint/template/recommended"],
"rules": {}
}
]
}

View File

@@ -1,70 +0,0 @@
# Specifies intentionally untracked files to ignore when using Git
# http://git-scm.com/docs/gitignore
*~
*.sw[mnpcod]
.tmp
*.tmp
*.tmp.*
UserInterfaceState.xcuserstate
$RECYCLE.BIN/
*.log
log.txt
/.sourcemaps
/.versions
/coverage
# Ionic
/.ionic
/www
/platforms
/plugins
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-project
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular
/.angular/cache
.sass-cache/
/.nx
/.nx/cache
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

View File

@@ -1,5 +0,0 @@
{
"recommendations": [
"ionic.ionic"
]
}

View File

@@ -1,3 +0,0 @@
{
"typescript.preferences.autoImportFileExcludePatterns": ["@ionic/angular/common", "@ionic/angular/standalone"]
}

View File

@@ -1,158 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"app": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "www",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "src/assets",
"output": "assets"
},
{
"glob": "**/*.svg",
"input": "node_modules/ionicons/dist/ionicons/svg",
"output": "./svg"
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/global.scss",
"src/theme/variables.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
},
"ci": {
"progress": false
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "app:build:production"
},
"development": {
"buildTarget": "app:build:development"
},
"ci": {
"progress": false
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "app:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "src/assets",
"output": "assets"
},
{
"glob": "**/*.svg",
"input": "node_modules/ionicons/dist/ionicons/svg",
"output": "./svg"
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/global.scss",
"src/theme/variables.scss"
],
"scripts": []
},
"configurations": {
"ci": {
"progress": false,
"watch": false
}
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}
}
},
"cli": {
"schematicCollections": [
"@ionic/angular-toolkit"
],
"analytics": false
},
"schematics": {
"@ionic/angular-toolkit:component": {
"styleext": "scss"
},
"@ionic/angular-toolkit:page": {
"styleext": "scss"
}
}
}

View File

@@ -1,9 +0,0 @@
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'io.ionic.starter',
appName: 'allowance-planner-v2',
webDir: 'www'
};
export default config;

View File

@@ -1,7 +0,0 @@
{
"name": "allowance-planner-v2",
"integrations": {
"capacitor": {}
},
"type": "angular"
}

View File

@@ -1,44 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/app'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,68 +0,0 @@
{
"name": "allowance-planner-v2",
"version": "0.0.1",
"author": "Ionic Framework",
"homepage": "https://ionicframework.com/",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"lint": "ng lint"
},
"private": true,
"dependencies": {
"@angular/animations": "^19.0.0",
"@angular/cdk": "^19.2.15",
"@angular/common": "^19.0.0",
"@angular/compiler": "^19.0.0",
"@angular/core": "^19.0.0",
"@angular/forms": "^19.0.0",
"@angular/material": "^19.2.15",
"@angular/platform-browser": "^19.0.0",
"@angular/platform-browser-dynamic": "^19.0.0",
"@angular/router": "^19.0.0",
"@capacitor/app": "7.0.1",
"@capacitor/core": "7.2.0",
"@capacitor/haptics": "7.0.1",
"@capacitor/keyboard": "7.0.1",
"@capacitor/status-bar": "7.0.1",
"@ionic/angular": "^8.0.0",
"@ionic/pwa-elements": "^3.3.0",
"@ionic/storage-angular": "^4.0.0",
"ionicons": "^7.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^19.0.0",
"@angular-eslint/builder": "^19.0.0",
"@angular-eslint/eslint-plugin": "^19.0.0",
"@angular-eslint/eslint-plugin-template": "^19.0.0",
"@angular-eslint/schematics": "^19.0.0",
"@angular-eslint/template-parser": "^19.0.0",
"@angular/cli": "^19.0.0",
"@angular/compiler-cli": "^19.0.0",
"@angular/language-service": "^19.0.0",
"@capacitor/cli": "7.2.0",
"@ionic/angular-toolkit": "^12.0.0",
"@types/jasmine": "~5.1.0",
"@typescript-eslint/eslint-plugin": "^8.18.0",
"@typescript-eslint/parser": "^8.18.0",
"eslint": "^9.16.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsdoc": "^48.2.1",
"eslint-plugin-prefer-arrow": "1.2.2",
"jasmine-core": "~5.1.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.6.3"
},
"description": "An Ionic project"
}

View File

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

View File

@@ -1,3 +0,0 @@
<ion-app>
<ion-router-outlet></ion-router-outlet>
</ion-app>

View File

@@ -1,21 +0,0 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
});

View File

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

View File

@@ -1,26 +0,0 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
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 { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
IonicModule.forRoot(),
AppRoutingModule,
IonicStorageModule.forRoot({
name: '__mydb',
driverOrder: [Drivers.IndexedDB, Drivers.LocalStorage]
})
],
providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
bootstrap: [AppComponent],
})
export class AppModule {}

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,16 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AllowancePage } from './allowance.page';
const routes: Routes = [
{
path: ':id',
component: AllowancePage,
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AllowancePageRoutingModule {}

View File

@@ -1,18 +0,0 @@
import { IonicModule } from '@ionic/angular';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { AllowancePage } from './allowance.page';
import { AllowancePageRoutingModule } from './allowance-routing.module';
@NgModule({
imports: [
IonicModule,
CommonModule,
FormsModule,
AllowancePageRoutingModule
],
declarations: [AllowancePage]
})
export class AllowancePageModule {}

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,26 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
import { AllowancePage } from './allowance.page';
describe('AllowancePage', () => {
let component: AllowancePage;
let fixture: ComponentFixture<AllowancePage>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AllowancePage],
imports: [IonicModule.forRoot(), ExploreContainerComponentModule]
}).compileComponents();
fixture = TestBed.createComponent(AllowancePage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,14 +0,0 @@
import { Component } from '@angular/core';
import { UserService } from 'src/app/services/user.service';
@Component({
selector: 'app-allowance',
templateUrl: 'allowance.page.html',
styleUrls: ['allowance.page.scss'],
standalone: false,
})
export class AllowancePage {
constructor(private userService: UserService) {}
}

View File

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

View File

@@ -1,18 +0,0 @@
import { IonicModule } from '@ionic/angular';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { HistoryPage } from './history.page';
import { HistoryPageRoutingModule } from './history-routing.module';
@NgModule({
imports: [
IonicModule,
CommonModule,
FormsModule,
HistoryPageRoutingModule
],
declarations: [HistoryPage]
})
export class HistoryPageModule {}

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,26 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
import { HistoryPage } from './history.page';
describe('HistoryPage', () => {
let component: HistoryPage;
let fixture: ComponentFixture<HistoryPage>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [HistoryPage],
imports: [IonicModule.forRoot(), ExploreContainerComponentModule]
}).compileComponents();
fixture = TestBed.createComponent(HistoryPage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

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

View File

@@ -1,39 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { TabsPage } from './tabs.page';
const routes: Routes = [
{
path: 'tabs',
component: TabsPage,
children: [
{
path: 'history',
loadChildren: () => import('../history/history.module').then(m => m.HistoryPageModule)
},
{
path: 'allowance',
loadChildren: () => import('../allowance/allowance.module').then(m => m.AllowancePageModule)
},
{
path: 'tasks',
loadChildren: () => import('../tasks/tasks.module').then(m => m.TasksPageModule)
},
{
path: '',
redirectTo: '/tabs/allowance',
pathMatch: 'full'
}
]
},
{
path: '',
redirectTo: '/tabs/allowance',
pathMatch: 'full'
}
];
@NgModule({
imports: [RouterModule.forChild(routes),],
})
export class TabsPageRoutingModule {}

View File

@@ -1,27 +0,0 @@
import { IonicModule } from '@ionic/angular';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import {MatIconModule} from '@angular/material/icon';
import { TabsPageRoutingModule } from './tabs-routing.module';
import { TabsPage } from './tabs.page';
import { provideHttpClient } from '@angular/common/http';
import { UserService } from 'src/app/services/user.service';
@NgModule({
imports: [
IonicModule,
CommonModule,
FormsModule,
TabsPageRoutingModule,
MatIconModule,
],
declarations: [TabsPage],
providers: [
provideHttpClient(),
UserService
]
})
export class TabsPageModule {}

View File

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

View File

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

View File

@@ -1,26 +0,0 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TabsPage } from './tabs.page';
describe('TabsPage', () => {
let component: TabsPage;
let fixture: ComponentFixture<TabsPage>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TabsPage],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TabsPage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

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

View File

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

View File

@@ -1,26 +0,0 @@
import { IonicModule } from '@ionic/angular';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { TasksPage } from './tasks.page';
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({
imports: [
IonicModule,
CommonModule,
FormsModule,
TasksPageRoutingModule,
MatIconModule,
],
declarations: [TasksPage],
providers: [
provideHttpClient(),
TaskService
]
})
export class TasksPageModule {}

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,26 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
import { TasksPage } from './tasks.page';
describe('TasksPage', () => {
let component: TasksPage;
let fixture: ComponentFixture<TasksPage>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TasksPage],
imports: [IonicModule.forRoot(), ExploreContainerComponentModule]
}).compileComponents();
fixture = TestBed.createComponent(TasksPage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

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}`);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 930 B

View File

@@ -1 +0,0 @@
<svg width="350" height="140" xmlns="http://www.w3.org/2000/svg" style="background:#f6f7f9"><g fill="none" fill-rule="evenodd"><path fill="#F04141" style="mix-blend-mode:multiply" d="M61.905-34.23l96.194 54.51-66.982 54.512L22 34.887z"/><circle fill="#10DC60" style="mix-blend-mode:multiply" cx="155.5" cy="135.5" r="57.5"/><path fill="#3880FF" style="mix-blend-mode:multiply" d="M208.538 9.513l84.417 15.392L223.93 93.93z"/><path fill="#FFCE00" style="mix-blend-mode:multiply" d="M268.625 106.557l46.332-26.75 46.332 26.75v53.5l-46.332 26.75-46.332-26.75z"/><circle fill="#7044FF" style="mix-blend-mode:multiply" cx="299.5" cy="9.5" r="38.5"/><rect fill="#11D3EA" style="mix-blend-mode:multiply" transform="rotate(-60 148.47 37.886)" x="143.372" y="-7.056" width="10.196" height="89.884" rx="5.098"/><path d="M-25.389 74.253l84.86 8.107c5.498.525 9.53 5.407 9.004 10.905a10 10 0 0 1-.057.477l-12.36 85.671a10.002 10.002 0 0 1-11.634 8.42l-86.351-15.226c-5.44-.959-9.07-6.145-8.112-11.584l13.851-78.551a10 10 0 0 1 10.799-8.219z" fill="#7044FF" style="mix-blend-mode:multiply"/><circle fill="#0CD1E8" style="mix-blend-mode:multiply" cx="273.5" cy="106.5" r="20.5"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,3 +0,0 @@
export const environment = {
production: true
};

View File

@@ -1,16 +0,0 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.

View File

@@ -1,45 +0,0 @@
/*
* 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,28 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Ionic App</title>
<base href="/" />
<meta name="color-scheme" content="light dark" />
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="format-detection" content="telephone=no" />
<meta name="msapplication-tap-highlight" content="no" />
<link rel="icon" type="image/png" href="assets/icon/favicon.png" />
<!-- add to homescreen for ios -->
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@@ -1,9 +0,0 @@
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { defineCustomElements } from '@ionic/pwa-elements/loader';
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));
// Call the element loader before the bootstrapModule/bootstrapApplication call
defineCustomElements(window);

View File

@@ -1,55 +0,0 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes recent versions of Safari, Chrome (including
* Opera), Edge on the desktop, and iOS and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
import './zone-flags';
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

View File

@@ -1,14 +0,0 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(),
);

View File

@@ -1,19 +0,0 @@
// For information on how to create your own theme, please see:
// 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');
}

View File

@@ -1,6 +0,0 @@
/**
* Prevents Angular change detection from
* running with certain Web Component callbacks
*/
// eslint-disable-next-line no-underscore-dangle
(window as any).__Zone_disable_customElements = true;

View File

@@ -1,15 +0,0 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.d.ts"
]
}

View File

@@ -1,33 +0,0 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "es2022",
"module": "es2020",
"lib": [
"es2018",
"dom"
],
"useDefineForClassFields": false
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

View File

@@ -1,18 +0,0 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"files": [
"src/test.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

View File

@@ -1,21 +0,0 @@
{
"name": "frontend",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@ionic/pwa-elements": "^3.3.0"
}
},
"node_modules/@ionic/pwa-elements": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@ionic/pwa-elements/-/pwa-elements-3.3.0.tgz",
"integrity": "sha512-vbykpxd2nGRlA67AnqDwsiVf8PUmInLyi6lQdnPDjeiML1WZa0CPe6r632nGDV9PTi+sWNde9Xexg9SD6Pwyqw==",
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
}
}
}
}

View File

@@ -1,5 +0,0 @@
{
"dependencies": {
"@ionic/pwa-elements": "^3.3.0"
}
}