Compare commits
30 Commits
14910d8a5f
...
19/post-al
| Author | SHA1 | Date | |
|---|---|---|---|
| b2f532fa22 | |||
| b56738653d | |||
| 5d803bb01c | |||
| 2620d6ee47 | |||
| 74536bd49d | |||
| 9cb71d53cf | |||
| b5aae3be3d | |||
| 238aedb5c9 | |||
| d1774c1ce0 | |||
| 8fedac21bb | |||
|
|
361baac8f3 | ||
|
|
0007f10ae3 | ||
|
|
b48d082edd | ||
|
|
bfc1d135de | ||
|
|
0749d8ce7a | ||
|
|
ef86deb222 | ||
|
|
6d6460ac3e | ||
| 1589bc9422 | |||
| 790ee3c622 | |||
| 6979368eda | |||
|
|
fd14c12a4a | ||
| cc817ed061 | |||
|
|
df1b8e4ed7 | ||
| 4355e1b1b7 | |||
|
|
2486bbf1ec | ||
|
|
b3e50dadb2 | ||
|
|
572c3c2a41 | ||
|
|
47f43cb0dc | ||
| 94a20af04d | |||
| f29eeae9d9 |
@@ -1,2 +1,9 @@
|
|||||||
# 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 .
|
||||||
|
```
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import (
|
|||||||
"github.com/gavv/httpexpect/v2"
|
"github.com/gavv/httpexpect/v2"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TestGoalName = "Test Goal"
|
TestAllowanceName = "Test History"
|
||||||
)
|
)
|
||||||
|
|
||||||
func startServer(t *testing.T) *httpexpect.Expect {
|
func startServer(t *testing.T) *httpexpect.Expect {
|
||||||
@@ -35,6 +36,7 @@ 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) {
|
||||||
@@ -47,56 +49,58 @@ func TestGetUserBadId(t *testing.T) {
|
|||||||
e.GET("/user/bad-id").Expect().Status(400)
|
e.GET("/user/bad-id").Expect().Status(400)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetUserGoalsWhenNoGoalsPresent(t *testing.T) {
|
func TestGetUserAllowanceWhenNoAllowancePresent(t *testing.T) {
|
||||||
e := startServer(t)
|
e := startServer(t)
|
||||||
result := e.GET("/user/1/goals").Expect().Status(200).JSON().Array()
|
result := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
|
||||||
result.Length().IsEqual(0)
|
result.Length().IsEqual(1)
|
||||||
|
item := result.Value(0).Object()
|
||||||
|
item.Value("id").IsEqual(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetUserGoals(t *testing.T) {
|
func TestGetUserAllowance(t *testing.T) {
|
||||||
e := startServer(t)
|
e := startServer(t)
|
||||||
|
|
||||||
// Create a new goal
|
// Create a new allowance
|
||||||
requestBody := map[string]interface{}{
|
requestBody := map[string]interface{}{
|
||||||
"name": TestGoalName,
|
"name": TestAllowanceName,
|
||||||
"target": 5000,
|
"target": 5000,
|
||||||
"weight": 10,
|
"weight": 10,
|
||||||
}
|
}
|
||||||
e.POST("/user/1/goals").WithJSON(requestBody).Expect().Status(201)
|
e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201)
|
||||||
|
|
||||||
// Validate goal
|
// Validate allowance
|
||||||
result := e.GET("/user/1/goals").Expect().Status(200).JSON().Array()
|
result := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
|
||||||
result.Length().IsEqual(1)
|
result.Length().IsEqual(2)
|
||||||
item := result.Value(0).Object()
|
item := result.Value(1).Object()
|
||||||
item.Value("id").IsEqual(1)
|
item.Value("id").IsEqual(1)
|
||||||
item.Value("name").IsEqual(TestGoalName)
|
item.Value("name").IsEqual(TestAllowanceName)
|
||||||
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 TestGetUserGoalsNoUser(t *testing.T) {
|
func TestGetUserAllowanceNoUser(t *testing.T) {
|
||||||
e := startServer(t)
|
e := startServer(t)
|
||||||
e.GET("/user/999/goals").Expect().Status(404)
|
e.GET("/user/999/allowance").Expect().Status(404)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetUserGoalsBadId(t *testing.T) {
|
func TestGetUserAllowanceBadId(t *testing.T) {
|
||||||
e := startServer(t)
|
e := startServer(t)
|
||||||
e.GET("/user/bad-id/goals").Expect().Status(400)
|
e.GET("/user/bad-id/allowance").Expect().Status(400)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateUserGoal(t *testing.T) {
|
func TestCreateUserAllowance(t *testing.T) {
|
||||||
e := startServer(t)
|
e := startServer(t)
|
||||||
|
|
||||||
// Create a new goal
|
// Create a new allowance
|
||||||
requestBody := map[string]interface{}{
|
requestBody := map[string]interface{}{
|
||||||
"name": TestGoalName,
|
"name": TestAllowanceName,
|
||||||
"target": 5000,
|
"target": 5000,
|
||||||
"weight": 10,
|
"weight": 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
response := e.POST("/user/1/goals").
|
response := e.POST("/user/1/allowance").
|
||||||
WithJSON(requestBody).
|
WithJSON(requestBody).
|
||||||
Expect().
|
Expect().
|
||||||
Status(201).
|
Status(201).
|
||||||
@@ -104,40 +108,40 @@ func TestCreateUserGoal(t *testing.T) {
|
|||||||
|
|
||||||
// Verify the response has an ID
|
// Verify the response has an ID
|
||||||
response.ContainsKey("id")
|
response.ContainsKey("id")
|
||||||
goalId := response.Value("id").Number().Raw()
|
allowanceId := response.Value("id").Number().Raw()
|
||||||
|
|
||||||
// Verify the goal exists in the list of goals
|
// Verify the allowance exists in the list of allowances
|
||||||
goals := e.GET("/user/1/goals").
|
allowances := e.GET("/user/1/allowance").
|
||||||
Expect().
|
Expect().
|
||||||
Status(200).
|
Status(200).
|
||||||
JSON().Array()
|
JSON().Array()
|
||||||
|
|
||||||
goals.Length().IsEqual(1)
|
allowances.Length().IsEqual(2)
|
||||||
|
|
||||||
goal := goals.Value(0).Object()
|
allowance := allowances.Value(1).Object()
|
||||||
goal.Value("id").IsEqual(goalId)
|
allowance.Value("id").IsEqual(allowanceId)
|
||||||
goal.Value("name").IsEqual(TestGoalName)
|
allowance.Value("name").IsEqual(TestAllowanceName)
|
||||||
goal.Value("target").IsEqual(5000)
|
allowance.Value("target").IsEqual(5000)
|
||||||
goal.Value("weight").IsEqual(10)
|
allowance.Value("weight").IsEqual(10)
|
||||||
goal.Value("progress").IsEqual(0)
|
allowance.Value("progress").IsEqual(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateUserGoalNoUser(t *testing.T) {
|
func TestCreateUserAllowanceNoUser(t *testing.T) {
|
||||||
e := startServer(t)
|
e := startServer(t)
|
||||||
|
|
||||||
requestBody := map[string]interface{}{
|
requestBody := map[string]interface{}{
|
||||||
"name": TestGoalName,
|
"name": TestAllowanceName,
|
||||||
"target": 5000,
|
"target": 5000,
|
||||||
"weight": 10,
|
"weight": 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
e.POST("/user/999/goals").
|
e.POST("/user/999/allowance").
|
||||||
WithJSON(requestBody).
|
WithJSON(requestBody).
|
||||||
Expect().
|
Expect().
|
||||||
Status(404)
|
Status(404)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateUserGoalInvalidInput(t *testing.T) {
|
func TestCreateUserAllowanceInvalidInput(t *testing.T) {
|
||||||
e := startServer(t)
|
e := startServer(t)
|
||||||
|
|
||||||
// Test with empty name
|
// Test with empty name
|
||||||
@@ -147,7 +151,7 @@ func TestCreateUserGoalInvalidInput(t *testing.T) {
|
|||||||
"weight": 10,
|
"weight": 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
e.POST("/user/1/goals").
|
e.POST("/user/1/allowance").
|
||||||
WithJSON(requestBody).
|
WithJSON(requestBody).
|
||||||
Expect().
|
Expect().
|
||||||
Status(400)
|
Status(400)
|
||||||
@@ -157,76 +161,81 @@ func TestCreateUserGoalInvalidInput(t *testing.T) {
|
|||||||
"target": 5000,
|
"target": 5000,
|
||||||
}
|
}
|
||||||
|
|
||||||
e.POST("/user/1/goals").
|
e.POST("/user/1/allowance").
|
||||||
WithJSON(invalidRequest).
|
WithJSON(invalidRequest).
|
||||||
Expect().
|
Expect().
|
||||||
Status(400)
|
Status(400)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateUserGoalBadId(t *testing.T) {
|
func TestCreateUserAllowanceBadId(t *testing.T) {
|
||||||
e := startServer(t)
|
e := startServer(t)
|
||||||
|
|
||||||
requestBody := map[string]interface{}{
|
requestBody := map[string]interface{}{
|
||||||
"name": TestGoalName,
|
"name": TestAllowanceName,
|
||||||
"target": 5000,
|
"target": 5000,
|
||||||
"weight": 10,
|
"weight": 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
e.POST("/user/bad-id/goals").
|
e.POST("/user/bad-id/allowance").
|
||||||
WithJSON(requestBody).
|
WithJSON(requestBody).
|
||||||
Expect().
|
Expect().
|
||||||
Status(400)
|
Status(400)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeleteUserGoal(t *testing.T) {
|
func TestDeleteUserAllowance(t *testing.T) {
|
||||||
e := startServer(t)
|
e := startServer(t)
|
||||||
|
|
||||||
// Create a new goal to delete
|
// Create a new allowance to delete
|
||||||
createRequest := map[string]interface{}{
|
createRequest := map[string]interface{}{
|
||||||
"name": TestGoalName,
|
"name": TestAllowanceName,
|
||||||
"target": 1000,
|
"target": 1000,
|
||||||
"weight": 5,
|
"weight": 5,
|
||||||
}
|
}
|
||||||
response := e.POST("/user/1/goals").
|
response := e.POST("/user/1/allowance").
|
||||||
WithJSON(createRequest).
|
WithJSON(createRequest).
|
||||||
Expect().
|
Expect().
|
||||||
Status(201).
|
Status(201).
|
||||||
JSON().Object()
|
JSON().Object()
|
||||||
|
|
||||||
goalId := response.Value("id").Number().Raw()
|
allowanceId := response.Value("id").Number().Raw()
|
||||||
|
|
||||||
// Delete the goal
|
// Delete the allowance
|
||||||
e.DELETE("/user/1/goal/" + strconv.Itoa(int(goalId))).
|
e.DELETE("/user/1/allowance/" + strconv.Itoa(int(allowanceId))).
|
||||||
Expect().
|
Expect().
|
||||||
Status(200).
|
Status(200).
|
||||||
JSON().Object().Value("message").IsEqual("Goal deleted successfully")
|
JSON().Object().Value("message").IsEqual("History deleted successfully")
|
||||||
|
|
||||||
// Verify the goal no longer exists
|
// Verify the allowance no longer exists
|
||||||
goals := e.GET("/user/1/goals").
|
allowances := e.GET("/user/1/allowance").
|
||||||
Expect().
|
Expect().
|
||||||
Status(200).
|
Status(200).
|
||||||
JSON().Array()
|
JSON().Array()
|
||||||
goals.Length().IsEqual(0)
|
allowances.Length().IsEqual(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeleteUserGoalNotFound(t *testing.T) {
|
func TestDeleteUserRestAllowance(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 goal
|
// Attempt to delete a non-existent allowance
|
||||||
e.DELETE("/user/1/goal/999").
|
e.DELETE("/user/1/allowance/999").
|
||||||
Expect().
|
Expect().
|
||||||
Status(404).
|
Status(404).
|
||||||
JSON().Object().Value("error").IsEqual("Goal not found")
|
JSON().Object().Value("error").IsEqual("History not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeleteUserGoalInvalidId(t *testing.T) {
|
func TestDeleteUserAllowanceInvalidId(t *testing.T) {
|
||||||
e := startServer(t)
|
e := startServer(t)
|
||||||
|
|
||||||
// Attempt to delete a goal with an invalid ID
|
// Attempt to delete an allowance with an invalid ID
|
||||||
e.DELETE("/user/1/goal/invalid-id").
|
e.DELETE("/user/1/allowance/invalid-id").
|
||||||
Expect().
|
Expect().
|
||||||
Status(400).
|
Status(400).
|
||||||
JSON().Object().Value("error").IsEqual("Invalid goal ID")
|
JSON().Object().Value("error").IsEqual("Invalid allowance ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateTask(t *testing.T) {
|
func TestCreateTask(t *testing.T) {
|
||||||
@@ -246,7 +255,16 @@ func TestCreateTask(t *testing.T) {
|
|||||||
|
|
||||||
// Verify the response has an ID
|
// Verify the response has an ID
|
||||||
response.ContainsKey("id")
|
response.ContainsKey("id")
|
||||||
taskId := response.Value("id").Number().Raw()
|
response.Value("id").Number().IsEqual(1)
|
||||||
|
|
||||||
|
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
|
||||||
@@ -263,7 +281,37 @@ func TestCreateTask(t *testing.T) {
|
|||||||
JSON().Object()
|
JSON().Object()
|
||||||
|
|
||||||
responseWithUser.ContainsKey("id")
|
responseWithUser.ContainsKey("id")
|
||||||
responseWithUser.Value("id").Number().NotEqual(taskId) // Ensure different ID
|
responseWithUser.Value("id").Number().IsEqual(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
@@ -313,15 +361,15 @@ func TestGetTaskWhenNoTasks(t *testing.T) {
|
|||||||
result.Length().IsEqual(0)
|
result.Length().IsEqual(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTestTask(e *httpexpect.Expect) {
|
func createTestTaskWithAmount(e *httpexpect.Expect, amount int) int {
|
||||||
requestBody := map[string]interface{}{
|
requestBody := map[string]interface{}{
|
||||||
"name": "Test Task",
|
"name": "Test Task",
|
||||||
"reward": 100,
|
"reward": amount,
|
||||||
}
|
}
|
||||||
e.POST("/tasks").WithJSON(requestBody).Expect().Status(201)
|
return int(e.POST("/tasks").WithJSON(requestBody).Expect().Status(201).JSON().Object().Value("id").Number().Raw())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetTaskSWhenTasks(t *testing.T) {
|
func TestGetTasksWhenTasks(t *testing.T) {
|
||||||
e := startServer(t)
|
e := startServer(t)
|
||||||
createTestTask(e)
|
createTestTask(e)
|
||||||
|
|
||||||
@@ -389,9 +437,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/allowance").WithJSON(PostAllowance{Allowance: 100}).Expect().Status(200)
|
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 100}).Expect().Status(200)
|
||||||
e.POST("/user/1/allowance").WithJSON(PostAllowance{Allowance: 20}).Expect().Status(200)
|
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 20}).Expect().Status(200)
|
||||||
e.POST("/user/1/allowance").WithJSON(PostAllowance{Allowance: -10}).Expect().Status(200)
|
e.POST("/user/1/history").WithJSON(PostHistory{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)
|
||||||
@@ -400,7 +448,262 @@ 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/allowance").WithJSON(PostAllowance{Allowance: 100}).Expect().
|
e.POST("/user/999/history").WithJSON(PostHistory{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)
|
||||||
}
|
}
|
||||||
|
|||||||
282
backend/db.go
282
backend/db.go
@@ -67,27 +67,53 @@ func (db *Db) UserExists(userId int) (bool, error) {
|
|||||||
return count > 0, nil
|
return count > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Db) GetUserGoals(userId int) ([]Goal, error) {
|
func (db *Db) GetUserAllowances(userId int) ([]Allowance, error) {
|
||||||
goals := make([]Goal, 0)
|
allowances := make([]Allowance, 0)
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
for row := range db.db.Query("select id, name, target, progress, weight from goals where user_id = ?").
|
totalAllowance := Allowance{}
|
||||||
|
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) {
|
||||||
goal := Goal{}
|
allowance := Allowance{}
|
||||||
err = row.Scan(&goal.ID, &goal.Name, &goal.Target, &goal.Progress, &goal.Weight)
|
err = row.Scan(&allowance.ID, &allowance.Name, &allowance.Target, &allowance.Progress, &allowance.Weight)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
goals = append(goals, goal)
|
allowances = append(allowances, allowance)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return goals, nil
|
return allowances, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Db) CreateGoal(userId int, goal *CreateGoalRequest) (int, error) {
|
func (db *Db) GetUserAllowanceById(userId int, allowanceId int) (*Allowance, error) {
|
||||||
// Check if user exists before attempting to create a goal
|
allowance := &Allowance{}
|
||||||
|
if allowanceId == 0 {
|
||||||
|
err := db.db.Query("select balance, weight from users where id = ?").
|
||||||
|
Bind(userId).ScanSingle(&allowance.Progress, &allowance.Weight)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := db.db.Query("select id, name, target, balance, weight from allowances where user_id = ? and id = ?").
|
||||||
|
Bind(userId, allowanceId).
|
||||||
|
ScanSingle(&allowance.ID, &allowance.Name, &allowance.Target, &allowance.Progress, &allowance.Weight)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allowance, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Db) CreateAllowance(userId int, allowance *CreateAllowanceRequest) (int, error) {
|
||||||
|
// 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
|
||||||
@@ -102,9 +128,9 @@ func (db *Db) CreateGoal(userId int, goal *CreateGoalRequest) (int, error) {
|
|||||||
}
|
}
|
||||||
defer tx.MustRollback()
|
defer tx.MustRollback()
|
||||||
|
|
||||||
// Insert the new goal
|
// Insert the new allowance
|
||||||
err = tx.Query("insert into goals (user_id, name, target, progress, weight) values (?, ?, ?, 0, ?)").
|
err = tx.Query("insert into allowances (user_id, name, target, weight) values (?, ?, ?, ?)").
|
||||||
Bind(userId, goal.Name, goal.Target, goal.Weight).
|
Bind(userId, allowance.Name, allowance.Target, allowance.Weight).
|
||||||
Exec()
|
Exec()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -127,21 +153,21 @@ func (db *Db) CreateGoal(userId int, goal *CreateGoalRequest) (int, error) {
|
|||||||
return lastId, nil
|
return lastId, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Db) DeleteGoal(userId int, goalId int) error {
|
func (db *Db) DeleteAllowance(userId int, allowanceId int) error {
|
||||||
// Check if the goal exists for the user
|
// Check if the allowance exists for the user
|
||||||
count := 0
|
count := 0
|
||||||
err := db.db.Query("select count(*) from goals where id = ? and user_id = ?").
|
err := db.db.Query("select count(*) from allowances where id = ? and user_id = ?").
|
||||||
Bind(goalId, userId).ScanSingle(&count)
|
Bind(allowanceId, userId).ScanSingle(&count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
return errors.New("goal not found")
|
return errors.New("allowance not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the goal
|
// Delete the allowance
|
||||||
err = db.db.Query("delete from goals where id = ? and user_id = ?").
|
err = db.db.Query("delete from allowances where id = ? and user_id = ?").
|
||||||
Bind(goalId, userId).Exec()
|
Bind(allowanceId, userId).Exec()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -149,6 +175,107 @@ func (db *Db) DeleteGoal(userId int, goalId 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 {
|
||||||
@@ -210,6 +337,21 @@ 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 = ?").
|
||||||
@@ -236,14 +378,89 @@ func (db *Db) UpdateTask(id int, task *CreateTaskRequest) error {
|
|||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Db) AddAllowance(userId int, allowance *PostAllowance) error {
|
func (db *Db) CompleteTask(taskId int) 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()
|
||||||
|
|
||||||
err = tx.Query("insert into history (user_id, date, amount) values (?, ?, ?)").
|
var reward int
|
||||||
|
err = tx.Query("select reward from tasks where id = ?").Bind(taskId).ScanSingle(&reward)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for userRow := range tx.Query("select id, weight from users").Range(&err) {
|
||||||
|
var userId int
|
||||||
|
var userWeight float64
|
||||||
|
err = userRow.Scan(&userId, &userWeight)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the history entry
|
||||||
|
err = tx.Query("insert into history (user_id, timestamp, amount) values (?, ?, ?)").
|
||||||
|
Bind(userId, time.Now().Unix(), reward).
|
||||||
|
Exec()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the sums of all weights
|
||||||
|
var sumOfWeights float64
|
||||||
|
err = tx.Query("select sum(weight) from allowances where user_id = ? and weight > 0").Bind(userId).ScanSingle(&sumOfWeights)
|
||||||
|
sumOfWeights += userWeight
|
||||||
|
|
||||||
|
remainingReward := reward
|
||||||
|
|
||||||
|
if sumOfWeights > 0 {
|
||||||
|
// Distribute the reward to the allowances
|
||||||
|
for allowanceRow := range tx.Query("select id, weight from allowances where user_id = ? and weight > 0").Bind(userId).Range(&err) {
|
||||||
|
var allowanceId int
|
||||||
|
var allowanceWeight float64
|
||||||
|
err = allowanceRow.Scan(&allowanceId, &allowanceWeight)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the amount to add to the allowance
|
||||||
|
amount := int((allowanceWeight / sumOfWeights) * float64(remainingReward))
|
||||||
|
sumOfWeights -= allowanceWeight
|
||||||
|
err = tx.Query("update allowances set balance = balance + ? where id = ? and user_id = ?").
|
||||||
|
Bind(amount, allowanceId, userId).Exec()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
remainingReward -= amount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the remaining reward to the user
|
||||||
|
err = tx.Query("update users set balance = balance + ? where id = ?").
|
||||||
|
Bind(remainingReward, userId).Exec()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the task
|
||||||
|
err = tx.Query("delete from tasks where id = ?").Bind(taskId).Exec()
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Db) AddHistory(userId int, allowance *PostHistory) error {
|
||||||
|
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 {
|
||||||
@@ -251,3 +468,24 @@ func (db *Db) AddAllowance(userId int, allowance *PostAllowance) 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, ×tamp)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
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"`
|
||||||
@@ -11,12 +13,12 @@ type UserWithAllowance struct {
|
|||||||
Allowance int `json:"allowance"`
|
Allowance int `json:"allowance"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Allowance struct {
|
type History struct {
|
||||||
Allowance int `json:"allowance"`
|
Allowance int `json:"allowance"`
|
||||||
Goals []Goal `json:"goals"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostAllowance struct {
|
type PostHistory struct {
|
||||||
Allowance int `json:"allowance"`
|
Allowance int `json:"allowance"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,18 +30,29 @@ type Task struct {
|
|||||||
Assigned *int `json:"assigned"` // Pointer to allow null
|
Assigned *int `json:"assigned"` // Pointer to allow null
|
||||||
}
|
}
|
||||||
|
|
||||||
type Goal struct {
|
type Allowance struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Target int `json:"target"`
|
Target int `json:"target"`
|
||||||
Progress int `json:"progress"`
|
Progress int `json:"progress"`
|
||||||
Weight int `json:"weight"`
|
Weight float64 `json:"weight"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateGoalRequest struct {
|
type CreateAllowanceRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Target int `json:"target"`
|
Target int `json:"target"`
|
||||||
Weight int `json:"weight"`
|
Weight float64 `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 {
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ module allowance_planner
|
|||||||
go 1.24.2
|
go 1.24.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.11.1
|
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0
|
||||||
github.com/gavv/httpexpect/v2 v2.17.0
|
github.com/gavv/httpexpect/v2 v2.17.0
|
||||||
|
github.com/gin-contrib/cors v1.7.5
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -67,9 +68,9 @@ require (
|
|||||||
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.65.2 // indirect
|
modernc.org/libc v1.65.7 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.10.0 // indirect
|
modernc.org/memory v1.11.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
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.11.1 h1:5s0r2IRpomGJC6pjirdMk7HAcAYEydLK5AhBZy+V1Ys=
|
gitea.seeseepuff.be/seeseemelk/mysqlite v0.12.0 h1:kl0VFgvm52UKxJhZpf1hvucxZdOoXY50g/VmzsWH+/8=
|
||||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.11.1/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
|
gitea.seeseepuff.be/seeseemelk/mysqlite v0.12.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
|
||||||
|
gitea.seeseepuff.be/seeseemelk/mysqlite v0.13.0 h1:nqSXu5i5fHB1rrx/kfi8Phn/J6eFa2yh02FiGc9U1yg=
|
||||||
|
gitea.seeseepuff.be/seeseemelk/mysqlite v0.13.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
|
||||||
|
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0 h1:aRItVfUj48fBmuec7rm/jY9KCfvHW2VzJfItVk4t8sw=
|
||||||
|
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
|
||||||
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4=
|
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4=
|
||||||
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w=
|
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w=
|
||||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||||
@@ -28,6 +32,8 @@ 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=
|
||||||
@@ -68,8 +74,9 @@ 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=
|
||||||
@@ -100,6 +107,8 @@ 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=
|
||||||
@@ -188,8 +197,9 @@ 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=
|
||||||
@@ -202,18 +212,22 @@ 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.27.1 h1:emhLB4uoOmkZUnTDFcMI3AbkmU/Evjuerit9Taqe6Ss=
|
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||||
modernc.org/ccgo/v4 v4.27.1/go.mod h1:543Q0qQhJWekKVS5P6yL5fO6liNhla9Lbm2/B3rEKDE=
|
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||||
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.2 h1:drWL1QO9fKXr3kXDN8y+4lKyBr8bA3mtUBQpftq3IJw=
|
modernc.org/libc v1.65.6 h1:OhJUhmuJ6MVZdqL5qmnd0/my46DKGFhSX4WOR7ijfyE=
|
||||||
modernc.org/libc v1.65.2/go.mod h1:VI3V2S5mNka4deJErQ0jsMXe7jgxojE2fOB/mWoHlbc=
|
modernc.org/libc v1.65.6/go.mod h1:MOiGAM9lrMBT9L8xT1nO41qYl5eg9gCp9/kWhz5L7WA=
|
||||||
|
modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
|
||||||
|
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
|
modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
|
||||||
modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
|
|||||||
335
backend/main.go
335
backend/main.go
@@ -11,6 +11,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/cors"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -75,7 +76,7 @@ func getUser(c *gin.Context) {
|
|||||||
c.IndentedJSON(http.StatusOK, user)
|
c.IndentedJSON(http.StatusOK, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserGoals(c *gin.Context) {
|
func getUserAllowance(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 {
|
||||||
@@ -96,16 +97,59 @@ func getUserGoals(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
goals, err := db.GetUserGoals(userId)
|
allowances, err := db.GetUserAllowances(userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error getting user goals: %v", err)
|
log.Printf("Error getting user allowance: %v", err)
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.IndentedJSON(http.StatusOK, goals)
|
c.IndentedJSON(http.StatusOK, allowances)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createUserGoal(c *gin.Context) {
|
func getUserAllowanceById(c *gin.Context) {
|
||||||
|
userIdStr := c.Param("userId")
|
||||||
|
allowanceIdStr := c.Param("allowanceId")
|
||||||
|
|
||||||
|
userId, err := strconv.Atoi(userIdStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(ErrInvalidUserID+": %v", err)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allowanceId, err := strconv.Atoi(allowanceIdStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Invalid allowance ID: %v", err)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid allowance ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := db.UserExists(userId)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(ErrCheckingUserExist, err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allowance, err := db.GetUserAllowanceById(userId, allowanceId)
|
||||||
|
if errors.Is(err, mysqlite.ErrNoRows) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Allowance not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error getting allowance: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.IndentedJSON(http.StatusOK, allowance)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createUserAllowance(c *gin.Context) {
|
||||||
userIdStr := c.Param("userId")
|
userIdStr := c.Param("userId")
|
||||||
userId, err := strconv.Atoi(userIdStr)
|
userId, err := strconv.Atoi(userIdStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -115,7 +159,7 @@ func createUserGoal(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse request body
|
// Parse request body
|
||||||
var goalRequest CreateGoalRequest
|
var goalRequest CreateAllowanceRequest
|
||||||
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"})
|
||||||
@@ -124,12 +168,12 @@ func createUserGoal(c *gin.Context) {
|
|||||||
|
|
||||||
// Validate request
|
// Validate request
|
||||||
if goalRequest.Name == "" {
|
if goalRequest.Name == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Goal name cannot be empty"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Allowance name cannot be empty"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create goal in database
|
// Create goal in database
|
||||||
goalId, err := db.CreateGoal(userId, &goalRequest)
|
goalId, err := db.CreateAllowance(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" {
|
||||||
@@ -145,9 +189,8 @@ func createUserGoal(c *gin.Context) {
|
|||||||
c.IndentedJSON(http.StatusCreated, response)
|
c.IndentedJSON(http.StatusCreated, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteUserGoal(c *gin.Context) {
|
func bulkPutUserAllowance(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 {
|
||||||
@@ -156,13 +199,6 @@ func deleteUserGoal(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)
|
||||||
@@ -174,18 +210,162 @@ func deleteUserGoal(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = db.DeleteGoal(userId, goalId)
|
var allowanceRequest []BulkUpdateAllowanceRequest
|
||||||
|
if err := c.ShouldBindJSON(&allowanceRequest); err != nil {
|
||||||
|
log.Printf("Error parsing request body: %v", err)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.BulkUpdateAllowance(userId, allowanceRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() == "goal not found" {
|
log.Printf("Error updating allowance: %v", err)
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
|
||||||
|
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 goal: %v", err)
|
log.Printf("Error deleting 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": "Goal deleted successfully"})
|
c.IndentedJSON(http.StatusOK, gin.H{"message": "History deleted successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func putUserAllowance(c *gin.Context) {
|
||||||
|
userIdStr := c.Param("userId")
|
||||||
|
allowanceIdStr := c.Param("allowanceId")
|
||||||
|
|
||||||
|
userId, err := strconv.Atoi(userIdStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(ErrInvalidUserID+": %v", err)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allowanceId, err := strconv.Atoi(allowanceIdStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Invalid allowance ID: %v", err)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid allowance ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := db.UserExists(userId)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(ErrCheckingUserExist, err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowanceRequest UpdateAllowanceRequest
|
||||||
|
if err := c.ShouldBindJSON(&allowanceRequest); err != nil {
|
||||||
|
log.Printf("Error parsing request body: %v", err)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if allowanceId == 0 {
|
||||||
|
err = db.UpdateUserAllowance(userId, &allowanceRequest)
|
||||||
|
} else {
|
||||||
|
err = db.UpdateAllowance(userId, allowanceId, &allowanceRequest)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error updating allowance: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.IndentedJSON(http.StatusOK, gin.H{"message": "Allowance updated successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func completeAllowance(c *gin.Context) {
|
||||||
|
userIdStr := c.Param("userId")
|
||||||
|
allowanceIdStr := c.Param("allowanceId")
|
||||||
|
|
||||||
|
userId, err := strconv.Atoi(userIdStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(ErrInvalidUserID+": %v", err)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allowanceId, err := strconv.Atoi(allowanceIdStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Invalid allowance ID: %v", err)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid allowance ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := db.UserExists(userId)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(ErrCheckingUserExist, err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.CompleteAllowance(userId, allowanceId)
|
||||||
|
if errors.Is(err, mysqlite.ErrNoRows) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Allowance not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error completing allowance: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.IndentedJSON(http.StatusOK, gin.H{"message": "Allowance completed successfully"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTask(c *gin.Context) {
|
func createTask(c *gin.Context) {
|
||||||
@@ -233,7 +413,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.JSON(http.StatusOK, &response)
|
c.IndentedJSON(http.StatusOK, &response)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTask(c *gin.Context) {
|
func getTask(c *gin.Context) {
|
||||||
@@ -289,7 +469,61 @@ 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 postAllowance(c *gin.Context) {
|
func deleteTask(c *gin.Context) {
|
||||||
|
taskIdStr := c.Param("taskId")
|
||||||
|
taskId, err := strconv.Atoi(taskIdStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Invalid task ID: %v", err)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hasTask, err := db.HasTask(taskId)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error checking task existence: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasTask {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.DeleteTask(taskId)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error deleting task: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Task deleted successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func completeTask(c *gin.Context) {
|
||||||
|
taskIdStr := c.Param("taskId")
|
||||||
|
taskId, err := strconv.Atoi(taskIdStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Invalid task ID: %v", err)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.CompleteTask(taskId)
|
||||||
|
if errors.Is(err, mysqlite.ErrNoRows) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error completing task: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Task completed successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func postHistory(c *gin.Context) {
|
||||||
userIdStr := c.Param("userId")
|
userIdStr := c.Param("userId")
|
||||||
userId, err := strconv.Atoi(userIdStr)
|
userId, err := strconv.Atoi(userIdStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -298,8 +532,8 @@ func postAllowance(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var allowanceRequest PostAllowance
|
var historyRequest PostHistory
|
||||||
if err := c.ShouldBindJSON(&allowanceRequest); err != nil {
|
if err := c.ShouldBindJSON(&historyRequest); 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
|
||||||
@@ -316,17 +550,35 @@ func postAllowance(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = db.AddAllowance(userId, &allowanceRequest)
|
err = db.AddHistory(userId, &historyRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error updating allowance: %v", err)
|
log.Printf("Error updating history: %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": "Allowance updated successfully"})
|
c.JSON(http.StatusOK, gin.H{"message": "History 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.
|
||||||
*/
|
*/
|
||||||
@@ -335,16 +587,26 @@ 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.GET("/api/user/:userId/goals", getUserGoals)
|
router.POST("/api/user/:userId/history", postHistory)
|
||||||
router.POST("/api/user/:userId/goals", createUserGoal)
|
router.GET("/api/user/:userId/history", getHistory)
|
||||||
router.DELETE("/api/user/:userId/goal/:goalId", deleteUserGoal)
|
router.GET("/api/user/:userId/allowance", getUserAllowance)
|
||||||
|
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.POST("/api/user/:userId/allowance", postAllowance)
|
router.DELETE("/api/task/:taskId", deleteTask)
|
||||||
|
router.POST("/api/task/:taskId/complete", completeTask)
|
||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: config.Addr,
|
Addr: config.Addr,
|
||||||
@@ -377,6 +639,11 @@ 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
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,
|
||||||
date date not null,
|
timestamp date not null,
|
||||||
amount integer not null
|
amount integer not null
|
||||||
);
|
);
|
||||||
|
|
||||||
create table goals
|
create table allowances
|
||||||
(
|
(
|
||||||
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,
|
||||||
progress integer not null,
|
balance integer not null default 0,
|
||||||
weight real not null
|
weight real not null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ paths:
|
|||||||
404:
|
404:
|
||||||
description: The users could not be found.
|
description: The users could not be found.
|
||||||
|
|
||||||
/user/{userId}/allowance:
|
/user/{userId}/history:
|
||||||
get:
|
get:
|
||||||
summary: Gets the allowance breakdown of a user
|
summary: Gets the allowance history of a user
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
name: userId
|
name: userId
|
||||||
@@ -71,18 +71,21 @@ paths:
|
|||||||
type: integer
|
type: integer
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: Information about the allowance of the user
|
description: Information about the allowance history of the user
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: array
|
||||||
properties:
|
items:
|
||||||
allowance:
|
type: object
|
||||||
type: integer
|
properties:
|
||||||
description: The total allowance value of the user, in cents.
|
date:
|
||||||
goals:
|
type: string
|
||||||
type: array
|
format: date-time
|
||||||
$ref: "#/components/schemas/goal"
|
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
|
||||||
post:
|
post:
|
||||||
summary: Updates the allowance of a user
|
summary: Updates the allowance of a user
|
||||||
parameters:
|
parameters:
|
||||||
@@ -111,35 +114,7 @@ paths:
|
|||||||
400:
|
400:
|
||||||
description: The allowance could not be updated.
|
description: The allowance could not be updated.
|
||||||
|
|
||||||
/user/{userId}/history:
|
/user/{userId}/allowance:
|
||||||
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:
|
||||||
@@ -226,7 +201,7 @@ paths:
|
|||||||
404:
|
404:
|
||||||
description: The goals could not be found.
|
description: The goals could not be found.
|
||||||
|
|
||||||
/user/{userId}/goal/{goalId}:
|
/user/{userId}/allowance/{goalId}:
|
||||||
get:
|
get:
|
||||||
summary: Gets information about a goal
|
summary: Gets information about a goal
|
||||||
parameters:
|
parameters:
|
||||||
@@ -309,7 +284,7 @@ paths:
|
|||||||
404:
|
404:
|
||||||
description: The goal could not be found.
|
description: The goal could not be found.
|
||||||
|
|
||||||
/user/{userId}/goal/{goalId}/complete:
|
/user/{userId}/allowance/{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.
|
||||||
|
|||||||
16
frontend/allowance-planner-v2/.browserslistrc
Normal file
16
frontend/allowance-planner-v2/.browserslistrc
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# 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
|
||||||
16
frontend/allowance-planner-v2/.editorconfig
Normal file
16
frontend/allowance-planner-v2/.editorconfig
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# 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
|
||||||
47
frontend/allowance-planner-v2/.eslintrc.json
Normal file
47
frontend/allowance-planner-v2/.eslintrc.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
70
frontend/allowance-planner-v2/.gitignore
vendored
Normal file
70
frontend/allowance-planner-v2/.gitignore
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# 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
|
||||||
5
frontend/allowance-planner-v2/.vscode/extensions.json
vendored
Normal file
5
frontend/allowance-planner-v2/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"ionic.ionic"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
frontend/allowance-planner-v2/.vscode/settings.json
vendored
Normal file
3
frontend/allowance-planner-v2/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"typescript.preferences.autoImportFileExcludePatterns": ["@ionic/angular/common", "@ionic/angular/standalone"]
|
||||||
|
}
|
||||||
158
frontend/allowance-planner-v2/angular.json
Normal file
158
frontend/allowance-planner-v2/angular.json
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
{
|
||||||
|
"$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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
frontend/allowance-planner-v2/capacitor.config.ts
Normal file
9
frontend/allowance-planner-v2/capacitor.config.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { CapacitorConfig } from '@capacitor/cli';
|
||||||
|
|
||||||
|
const config: CapacitorConfig = {
|
||||||
|
appId: 'io.ionic.starter',
|
||||||
|
appName: 'allowance-planner-v2',
|
||||||
|
webDir: 'www'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
7
frontend/allowance-planner-v2/ionic.config.json
Normal file
7
frontend/allowance-planner-v2/ionic.config.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "allowance-planner-v2",
|
||||||
|
"integrations": {
|
||||||
|
"capacitor": {}
|
||||||
|
},
|
||||||
|
"type": "angular"
|
||||||
|
}
|
||||||
44
frontend/allowance-planner-v2/karma.conf.js
Normal file
44
frontend/allowance-planner-v2/karma.conf.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// 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
|
||||||
|
});
|
||||||
|
};
|
||||||
17976
frontend/allowance-planner-v2/package-lock.json
generated
Normal file
17976
frontend/allowance-planner-v2/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
68
frontend/allowance-planner-v2/package.json
Normal file
68
frontend/allowance-planner-v2/package.json
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
22
frontend/allowance-planner-v2/src/app/app-routing.module.ts
Normal file
22
frontend/allowance-planner-v2/src/app/app-routing.module.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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 {}
|
||||||
3
frontend/allowance-planner-v2/src/app/app.component.html
Normal file
3
frontend/allowance-planner-v2/src/app/app.component.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<ion-app>
|
||||||
|
<ion-router-outlet></ion-router-outlet>
|
||||||
|
</ion-app>
|
||||||
21
frontend/allowance-planner-v2/src/app/app.component.spec.ts
Normal file
21
frontend/allowance-planner-v2/src/app/app.component.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
21
frontend/allowance-planner-v2/src/app/app.component.ts
Normal file
21
frontend/allowance-planner-v2/src/app/app.component.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
26
frontend/allowance-planner-v2/src/app/app.module.ts
Normal file
26
frontend/allowance-planner-v2/src/app/app.module.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
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 {}
|
||||||
6
frontend/allowance-planner-v2/src/app/models/task.ts
Normal file
6
frontend/allowance-planner-v2/src/app/models/task.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface Task {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
reward: number;
|
||||||
|
assigned: number;
|
||||||
|
}
|
||||||
5
frontend/allowance-planner-v2/src/app/models/user.ts
Normal file
5
frontend/allowance-planner-v2/src/app/models/user.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface User {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
allowance?: number
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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 {}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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 {}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<ion-header [translucent]="true" class="ion-no-border">
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title>
|
||||||
|
Allowance
|
||||||
|
</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
|
||||||
|
<ion-content>
|
||||||
|
</ion-content>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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) {}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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 {}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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 {}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<ion-header [translucent]="true" class="ion-no-border">
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title>
|
||||||
|
History
|
||||||
|
</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
|
||||||
|
<ion-content>
|
||||||
|
|
||||||
|
</ion-content>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-history',
|
||||||
|
templateUrl: 'history.page.html',
|
||||||
|
styleUrls: ['history.page.scss'],
|
||||||
|
standalone: false,
|
||||||
|
})
|
||||||
|
export class HistoryPage {
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
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 {}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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 {}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.tab-selected {
|
||||||
|
background-color: var(--ion-color-secondary);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-tabs',
|
||||||
|
templateUrl: 'tabs.page.html',
|
||||||
|
styleUrls: ['tabs.page.scss'],
|
||||||
|
standalone: false,
|
||||||
|
})
|
||||||
|
export class TabsPage {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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 {}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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 {}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
.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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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 {}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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 {}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
.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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
frontend/allowance-planner-v2/src/assets/font/Jaro-Regular.ttf
Normal file
BIN
frontend/allowance-planner-v2/src/assets/font/Jaro-Regular.ttf
Normal file
Binary file not shown.
BIN
frontend/allowance-planner-v2/src/assets/icon/favicon.png
Normal file
BIN
frontend/allowance-planner-v2/src/assets/icon/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 930 B |
1
frontend/allowance-planner-v2/src/assets/shapes.svg
Normal file
1
frontend/allowance-planner-v2/src/assets/shapes.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
export const environment = {
|
||||||
|
production: true
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// 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.
|
||||||
45
frontend/allowance-planner-v2/src/global.scss
Normal file
45
frontend/allowance-planner-v2/src/global.scss
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
28
frontend/allowance-planner-v2/src/index.html
Normal file
28
frontend/allowance-planner-v2/src/index.html
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<!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>
|
||||||
9
frontend/allowance-planner-v2/src/main.ts
Normal file
9
frontend/allowance-planner-v2/src/main.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
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);
|
||||||
55
frontend/allowance-planner-v2/src/polyfills.ts
Normal file
55
frontend/allowance-planner-v2/src/polyfills.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
14
frontend/allowance-planner-v2/src/test.ts
Normal file
14
frontend/allowance-planner-v2/src/test.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// 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(),
|
||||||
|
);
|
||||||
19
frontend/allowance-planner-v2/src/theme/variables.scss
Normal file
19
frontend/allowance-planner-v2/src/theme/variables.scss
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// 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');
|
||||||
|
}
|
||||||
6
frontend/allowance-planner-v2/src/zone-flags.ts
Normal file
6
frontend/allowance-planner-v2/src/zone-flags.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
15
frontend/allowance-planner-v2/tsconfig.app.json
Normal file
15
frontend/allowance-planner-v2/tsconfig.app.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/* 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
33
frontend/allowance-planner-v2/tsconfig.json
Normal file
33
frontend/allowance-planner-v2/tsconfig.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/* 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
|
||||||
|
}
|
||||||
|
}
|
||||||
18
frontend/allowance-planner-v2/tsconfig.spec.json
Normal file
18
frontend/allowance-planner-v2/tsconfig.spec.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/* 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
21
frontend/package-lock.json
generated
Normal file
21
frontend/package-lock.json
generated
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
frontend/package.json
Normal file
5
frontend/package.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@ionic/pwa-elements": "^3.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user