Compare commits
	
		
			29 Commits
		
	
	
		
			c81a0d3294
			...
			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 | 
@@ -1,2 +1,9 @@
 | 
			
		||||
# Allowance Planner 2000
 | 
			
		||||
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"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	TestGoalName = "Test Goal"
 | 
			
		||||
	TestAllowanceName = "Test History"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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.Value("name").IsEqual("Seeseemelk")
 | 
			
		||||
	result.Value("id").IsEqual(1)
 | 
			
		||||
	result.Value("allowance").IsEqual(0)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetUserUnknown(t *testing.T) {
 | 
			
		||||
@@ -47,56 +49,58 @@ func TestGetUserBadId(t *testing.T) {
 | 
			
		||||
	e.GET("/user/bad-id").Expect().Status(400)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetUserGoalsWhenNoGoalsPresent(t *testing.T) {
 | 
			
		||||
func TestGetUserAllowanceWhenNoAllowancePresent(t *testing.T) {
 | 
			
		||||
	e := startServer(t)
 | 
			
		||||
	result := e.GET("/user/1/goals").Expect().Status(200).JSON().Array()
 | 
			
		||||
	result.Length().IsEqual(0)
 | 
			
		||||
	result := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
 | 
			
		||||
	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)
 | 
			
		||||
 | 
			
		||||
	// Create a new goal
 | 
			
		||||
	// Create a new allowance
 | 
			
		||||
	requestBody := map[string]interface{}{
 | 
			
		||||
		"name":   TestGoalName,
 | 
			
		||||
		"name":   TestAllowanceName,
 | 
			
		||||
		"target": 5000,
 | 
			
		||||
		"weight": 10,
 | 
			
		||||
	}
 | 
			
		||||
	e.POST("/user/1/goals").WithJSON(requestBody).Expect().Status(201)
 | 
			
		||||
	e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201)
 | 
			
		||||
 | 
			
		||||
	// Validate goal
 | 
			
		||||
	result := e.GET("/user/1/goals").Expect().Status(200).JSON().Array()
 | 
			
		||||
	result.Length().IsEqual(1)
 | 
			
		||||
	item := result.Value(0).Object()
 | 
			
		||||
	// Validate allowance
 | 
			
		||||
	result := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
 | 
			
		||||
	result.Length().IsEqual(2)
 | 
			
		||||
	item := result.Value(1).Object()
 | 
			
		||||
	item.Value("id").IsEqual(1)
 | 
			
		||||
	item.Value("name").IsEqual(TestGoalName)
 | 
			
		||||
	item.Value("name").IsEqual(TestAllowanceName)
 | 
			
		||||
	item.Value("target").IsEqual(5000)
 | 
			
		||||
	item.Value("weight").IsEqual(10)
 | 
			
		||||
	item.Value("progress").IsEqual(0)
 | 
			
		||||
	item.NotContainsKey("user_id")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetUserGoalsNoUser(t *testing.T) {
 | 
			
		||||
func TestGetUserAllowanceNoUser(t *testing.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.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)
 | 
			
		||||
 | 
			
		||||
	// Create a new goal
 | 
			
		||||
	// Create a new allowance
 | 
			
		||||
	requestBody := map[string]interface{}{
 | 
			
		||||
		"name":   TestGoalName,
 | 
			
		||||
		"name":   TestAllowanceName,
 | 
			
		||||
		"target": 5000,
 | 
			
		||||
		"weight": 10,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	response := e.POST("/user/1/goals").
 | 
			
		||||
	response := e.POST("/user/1/allowance").
 | 
			
		||||
		WithJSON(requestBody).
 | 
			
		||||
		Expect().
 | 
			
		||||
		Status(201).
 | 
			
		||||
@@ -104,40 +108,40 @@ func TestCreateUserGoal(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
	// Verify the response has an 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
 | 
			
		||||
	goals := e.GET("/user/1/goals").
 | 
			
		||||
	// Verify the allowance exists in the list of allowances
 | 
			
		||||
	allowances := e.GET("/user/1/allowance").
 | 
			
		||||
		Expect().
 | 
			
		||||
		Status(200).
 | 
			
		||||
		JSON().Array()
 | 
			
		||||
 | 
			
		||||
	goals.Length().IsEqual(1)
 | 
			
		||||
	allowances.Length().IsEqual(2)
 | 
			
		||||
 | 
			
		||||
	goal := goals.Value(0).Object()
 | 
			
		||||
	goal.Value("id").IsEqual(goalId)
 | 
			
		||||
	goal.Value("name").IsEqual(TestGoalName)
 | 
			
		||||
	goal.Value("target").IsEqual(5000)
 | 
			
		||||
	goal.Value("weight").IsEqual(10)
 | 
			
		||||
	goal.Value("progress").IsEqual(0)
 | 
			
		||||
	allowance := allowances.Value(1).Object()
 | 
			
		||||
	allowance.Value("id").IsEqual(allowanceId)
 | 
			
		||||
	allowance.Value("name").IsEqual(TestAllowanceName)
 | 
			
		||||
	allowance.Value("target").IsEqual(5000)
 | 
			
		||||
	allowance.Value("weight").IsEqual(10)
 | 
			
		||||
	allowance.Value("progress").IsEqual(0)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCreateUserGoalNoUser(t *testing.T) {
 | 
			
		||||
func TestCreateUserAllowanceNoUser(t *testing.T) {
 | 
			
		||||
	e := startServer(t)
 | 
			
		||||
 | 
			
		||||
	requestBody := map[string]interface{}{
 | 
			
		||||
		"name":   TestGoalName,
 | 
			
		||||
		"name":   TestAllowanceName,
 | 
			
		||||
		"target": 5000,
 | 
			
		||||
		"weight": 10,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	e.POST("/user/999/goals").
 | 
			
		||||
	e.POST("/user/999/allowance").
 | 
			
		||||
		WithJSON(requestBody).
 | 
			
		||||
		Expect().
 | 
			
		||||
		Status(404)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCreateUserGoalInvalidInput(t *testing.T) {
 | 
			
		||||
func TestCreateUserAllowanceInvalidInput(t *testing.T) {
 | 
			
		||||
	e := startServer(t)
 | 
			
		||||
 | 
			
		||||
	// Test with empty name
 | 
			
		||||
@@ -147,7 +151,7 @@ func TestCreateUserGoalInvalidInput(t *testing.T) {
 | 
			
		||||
		"weight": 10,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	e.POST("/user/1/goals").
 | 
			
		||||
	e.POST("/user/1/allowance").
 | 
			
		||||
		WithJSON(requestBody).
 | 
			
		||||
		Expect().
 | 
			
		||||
		Status(400)
 | 
			
		||||
@@ -157,76 +161,81 @@ func TestCreateUserGoalInvalidInput(t *testing.T) {
 | 
			
		||||
		"target": 5000,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	e.POST("/user/1/goals").
 | 
			
		||||
	e.POST("/user/1/allowance").
 | 
			
		||||
		WithJSON(invalidRequest).
 | 
			
		||||
		Expect().
 | 
			
		||||
		Status(400)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCreateUserGoalBadId(t *testing.T) {
 | 
			
		||||
func TestCreateUserAllowanceBadId(t *testing.T) {
 | 
			
		||||
	e := startServer(t)
 | 
			
		||||
 | 
			
		||||
	requestBody := map[string]interface{}{
 | 
			
		||||
		"name":   TestGoalName,
 | 
			
		||||
		"name":   TestAllowanceName,
 | 
			
		||||
		"target": 5000,
 | 
			
		||||
		"weight": 10,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	e.POST("/user/bad-id/goals").
 | 
			
		||||
	e.POST("/user/bad-id/allowance").
 | 
			
		||||
		WithJSON(requestBody).
 | 
			
		||||
		Expect().
 | 
			
		||||
		Status(400)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDeleteUserGoal(t *testing.T) {
 | 
			
		||||
func TestDeleteUserAllowance(t *testing.T) {
 | 
			
		||||
	e := startServer(t)
 | 
			
		||||
 | 
			
		||||
	// Create a new goal to delete
 | 
			
		||||
	// Create a new allowance to delete
 | 
			
		||||
	createRequest := map[string]interface{}{
 | 
			
		||||
		"name":   TestGoalName,
 | 
			
		||||
		"name":   TestAllowanceName,
 | 
			
		||||
		"target": 1000,
 | 
			
		||||
		"weight": 5,
 | 
			
		||||
	}
 | 
			
		||||
	response := e.POST("/user/1/goals").
 | 
			
		||||
	response := e.POST("/user/1/allowance").
 | 
			
		||||
		WithJSON(createRequest).
 | 
			
		||||
		Expect().
 | 
			
		||||
		Status(201).
 | 
			
		||||
		JSON().Object()
 | 
			
		||||
 | 
			
		||||
	goalId := response.Value("id").Number().Raw()
 | 
			
		||||
	allowanceId := response.Value("id").Number().Raw()
 | 
			
		||||
 | 
			
		||||
	// Delete the goal
 | 
			
		||||
	e.DELETE("/user/1/goal/" + strconv.Itoa(int(goalId))).
 | 
			
		||||
	// Delete the allowance
 | 
			
		||||
	e.DELETE("/user/1/allowance/" + strconv.Itoa(int(allowanceId))).
 | 
			
		||||
		Expect().
 | 
			
		||||
		Status(200).
 | 
			
		||||
		JSON().Object().Value("message").IsEqual("Goal deleted successfully")
 | 
			
		||||
		JSON().Object().Value("message").IsEqual("History deleted successfully")
 | 
			
		||||
 | 
			
		||||
	// Verify the goal no longer exists
 | 
			
		||||
	goals := e.GET("/user/1/goals").
 | 
			
		||||
	// Verify the allowance no longer exists
 | 
			
		||||
	allowances := e.GET("/user/1/allowance").
 | 
			
		||||
		Expect().
 | 
			
		||||
		Status(200).
 | 
			
		||||
		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)
 | 
			
		||||
 | 
			
		||||
	// Attempt to delete a non-existent goal
 | 
			
		||||
	e.DELETE("/user/1/goal/999").
 | 
			
		||||
	// Attempt to delete a non-existent allowance
 | 
			
		||||
	e.DELETE("/user/1/allowance/999").
 | 
			
		||||
		Expect().
 | 
			
		||||
		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)
 | 
			
		||||
 | 
			
		||||
	// Attempt to delete a goal with an invalid ID
 | 
			
		||||
	e.DELETE("/user/1/goal/invalid-id").
 | 
			
		||||
	// Attempt to delete an allowance with an invalid ID
 | 
			
		||||
	e.DELETE("/user/1/allowance/invalid-id").
 | 
			
		||||
		Expect().
 | 
			
		||||
		Status(400).
 | 
			
		||||
		JSON().Object().Value("error").IsEqual("Invalid goal ID")
 | 
			
		||||
		JSON().Object().Value("error").IsEqual("Invalid allowance ID")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCreateTask(t *testing.T) {
 | 
			
		||||
@@ -246,7 +255,16 @@ func TestCreateTask(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
	// Verify the response has an 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
 | 
			
		||||
	assignedUserId := 1
 | 
			
		||||
@@ -263,7 +281,37 @@ func TestCreateTask(t *testing.T) {
 | 
			
		||||
		JSON().Object()
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
@@ -313,15 +361,15 @@ func TestGetTaskWhenNoTasks(t *testing.T) {
 | 
			
		||||
	result.Length().IsEqual(0)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func createTestTask(e *httpexpect.Expect) {
 | 
			
		||||
func createTestTaskWithAmount(e *httpexpect.Expect, amount int) int {
 | 
			
		||||
	requestBody := map[string]interface{}{
 | 
			
		||||
		"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)
 | 
			
		||||
	createTestTask(e)
 | 
			
		||||
 | 
			
		||||
@@ -389,9 +437,9 @@ func TestPutTaskInvalidTaskId(t *testing.T) {
 | 
			
		||||
func TestPostAllowance(t *testing.T) {
 | 
			
		||||
	e := startServer(t)
 | 
			
		||||
 | 
			
		||||
	e.POST("/user/1/allowance").WithJSON(PostAllowance{Allowance: 100}).Expect().Status(200)
 | 
			
		||||
	e.POST("/user/1/allowance").WithJSON(PostAllowance{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: 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").Expect().Status(200).JSON().Object()
 | 
			
		||||
	response.Value("allowance").Number().IsEqual(100 + 20 - 10)
 | 
			
		||||
@@ -400,7 +448,262 @@ func TestPostAllowance(t *testing.T) {
 | 
			
		||||
func TestPostAllowanceInvalidUserId(t *testing.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)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										287
									
								
								backend/db.go
									
									
									
									
									
								
							
							
						
						
									
										287
									
								
								backend/db.go
									
									
									
									
									
								
							@@ -49,11 +49,8 @@ func (db *Db) GetUsers() ([]User, error) {
 | 
			
		||||
func (db *Db) GetUser(id int) (*UserWithAllowance, error) {
 | 
			
		||||
	user := &UserWithAllowance{}
 | 
			
		||||
 | 
			
		||||
	err := db.db.Query("select u.id, u.name, sum(h.amount) from users u join history h on h.user_id = u.id where u.id = ?").
 | 
			
		||||
	err := db.db.Query("select u.id, u.name, (select ifnull(sum(h.amount), 0) from history h where h.user_id = u.id) from users u where u.id = ?").
 | 
			
		||||
		Bind(id).ScanSingle(&user.ID, &user.Name, &user.Allowance)
 | 
			
		||||
	if errors.Is(err, mysqlite.ErrNoRows) {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
@@ -70,27 +67,53 @@ func (db *Db) UserExists(userId int) (bool, error) {
 | 
			
		||||
	return count > 0, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (db *Db) GetUserGoals(userId int) ([]Goal, error) {
 | 
			
		||||
	goals := make([]Goal, 0)
 | 
			
		||||
func (db *Db) GetUserAllowances(userId int) ([]Allowance, error) {
 | 
			
		||||
	allowances := make([]Allowance, 0)
 | 
			
		||||
	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) {
 | 
			
		||||
		goal := Goal{}
 | 
			
		||||
		err = row.Scan(&goal.ID, &goal.Name, &goal.Target, &goal.Progress, &goal.Weight)
 | 
			
		||||
		allowance := Allowance{}
 | 
			
		||||
		err = row.Scan(&allowance.ID, &allowance.Name, &allowance.Target, &allowance.Progress, &allowance.Weight)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		goals = append(goals, goal)
 | 
			
		||||
		allowances = append(allowances, allowance)
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return goals, nil
 | 
			
		||||
	return allowances, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (db *Db) CreateGoal(userId int, goal *CreateGoalRequest) (int, error) {
 | 
			
		||||
	// Check if user exists before attempting to create a goal
 | 
			
		||||
func (db *Db) GetUserAllowanceById(userId int, allowanceId int) (*Allowance, error) {
 | 
			
		||||
	allowance := &Allowance{}
 | 
			
		||||
	if allowanceId == 0 {
 | 
			
		||||
		err := db.db.Query("select balance, weight from users where id = ?").
 | 
			
		||||
			Bind(userId).ScanSingle(&allowance.Progress, &allowance.Weight)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		err := db.db.Query("select id, name, target, balance, weight from allowances where user_id = ? and id = ?").
 | 
			
		||||
			Bind(userId, allowanceId).
 | 
			
		||||
			ScanSingle(&allowance.ID, &allowance.Name, &allowance.Target, &allowance.Progress, &allowance.Weight)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return allowance, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (db *Db) CreateAllowance(userId int, allowance *CreateAllowanceRequest) (int, error) {
 | 
			
		||||
	// Check if user exists before attempting to create an allowance
 | 
			
		||||
	exists, err := db.UserExists(userId)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
@@ -105,9 +128,9 @@ func (db *Db) CreateGoal(userId int, goal *CreateGoalRequest) (int, error) {
 | 
			
		||||
	}
 | 
			
		||||
	defer tx.MustRollback()
 | 
			
		||||
 | 
			
		||||
	// Insert the new goal
 | 
			
		||||
	err = tx.Query("insert into goals (user_id, name, target, progress, weight) values (?, ?, ?, 0, ?)").
 | 
			
		||||
		Bind(userId, goal.Name, goal.Target, goal.Weight).
 | 
			
		||||
	// Insert the new allowance
 | 
			
		||||
	err = tx.Query("insert into allowances (user_id, name, target, weight) values (?, ?, ?, ?)").
 | 
			
		||||
		Bind(userId, allowance.Name, allowance.Target, allowance.Weight).
 | 
			
		||||
		Exec()
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -130,21 +153,21 @@ func (db *Db) CreateGoal(userId int, goal *CreateGoalRequest) (int, error) {
 | 
			
		||||
	return lastId, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (db *Db) DeleteGoal(userId int, goalId int) error {
 | 
			
		||||
	// Check if the goal exists for the user
 | 
			
		||||
func (db *Db) DeleteAllowance(userId int, allowanceId int) error {
 | 
			
		||||
	// Check if the allowance exists for the user
 | 
			
		||||
	count := 0
 | 
			
		||||
	err := db.db.Query("select count(*) from goals where id = ? and user_id = ?").
 | 
			
		||||
		Bind(goalId, userId).ScanSingle(&count)
 | 
			
		||||
	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("goal not found")
 | 
			
		||||
		return errors.New("allowance not found")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Delete the goal
 | 
			
		||||
	err = db.db.Query("delete from goals where id = ? and user_id = ?").
 | 
			
		||||
		Bind(goalId, userId).Exec()
 | 
			
		||||
	// Delete the allowance
 | 
			
		||||
	err = db.db.Query("delete from allowances where id = ? and user_id = ?").
 | 
			
		||||
		Bind(allowanceId, userId).Exec()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -152,6 +175,107 @@ func (db *Db) DeleteGoal(userId int, goalId int) error {
 | 
			
		||||
	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) {
 | 
			
		||||
	tx, err := db.db.Begin()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -213,6 +337,21 @@ func (db *Db) GetTask(id int) (Task, error) {
 | 
			
		||||
	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) {
 | 
			
		||||
	count := 0
 | 
			
		||||
	err := db.db.Query("select count(*) from tasks where id = ?").
 | 
			
		||||
@@ -239,14 +378,89 @@ func (db *Db) UpdateTask(id int, task *CreateTaskRequest) error {
 | 
			
		||||
	return tx.Commit()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (db *Db) AddAllowance(userId int, allowance *PostAllowance) error {
 | 
			
		||||
func (db *Db) CompleteTask(taskId int) error {
 | 
			
		||||
	tx, err := db.db.Begin()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	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).
 | 
			
		||||
		Exec()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -254,3 +468,24 @@ func (db *Db) AddAllowance(userId int, allowance *PostAllowance) error {
 | 
			
		||||
	}
 | 
			
		||||
	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
 | 
			
		||||
 | 
			
		||||
import "time"
 | 
			
		||||
 | 
			
		||||
type User struct {
 | 
			
		||||
	ID   int    `json:"id"`
 | 
			
		||||
	Name string `json:"name"`
 | 
			
		||||
@@ -11,12 +13,12 @@ type UserWithAllowance struct {
 | 
			
		||||
	Allowance int    `json:"allowance"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Allowance struct {
 | 
			
		||||
type History struct {
 | 
			
		||||
	Allowance int       `json:"allowance"`
 | 
			
		||||
	Goals     []Goal `json:"goals"`
 | 
			
		||||
	Timestamp time.Time `json:"timestamp"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PostAllowance struct {
 | 
			
		||||
type PostHistory struct {
 | 
			
		||||
	Allowance int `json:"allowance"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -28,18 +30,29 @@ type Task struct {
 | 
			
		||||
	Assigned *int   `json:"assigned"` // Pointer to allow null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Goal struct {
 | 
			
		||||
type Allowance struct {
 | 
			
		||||
	ID       int     `json:"id"`
 | 
			
		||||
	Name     string  `json:"name"`
 | 
			
		||||
	Target   int     `json:"target"`
 | 
			
		||||
	Progress int     `json:"progress"`
 | 
			
		||||
	Weight   int    `json:"weight"`
 | 
			
		||||
	Weight   float64 `json:"weight"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CreateGoalRequest struct {
 | 
			
		||||
type CreateAllowanceRequest struct {
 | 
			
		||||
	Name   string  `json:"name"`
 | 
			
		||||
	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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,9 @@ module allowance_planner
 | 
			
		||||
go 1.24.2
 | 
			
		||||
 | 
			
		||||
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/gin-contrib/cors v1.7.5
 | 
			
		||||
	github.com/gin-gonic/gin v1.10.0
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -67,9 +68,9 @@ require (
 | 
			
		||||
	gopkg.in/fsnotify.v1 v1.4.7 // indirect
 | 
			
		||||
	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // 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/memory v1.10.0 // indirect
 | 
			
		||||
	modernc.org/memory v1.11.0 // indirect
 | 
			
		||||
	modernc.org/sqlite v1.37.0 // indirect
 | 
			
		||||
	moul.io/http2curl/v2 v2.3.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.11.1/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
 | 
			
		||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.12.0 h1:kl0VFgvm52UKxJhZpf1hvucxZdOoXY50g/VmzsWH+/8=
 | 
			
		||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.12.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
 | 
			
		||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.13.0 h1:nqSXu5i5fHB1rrx/kfi8Phn/J6eFa2yh02FiGc9U1yg=
 | 
			
		||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.13.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
 | 
			
		||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0 h1:aRItVfUj48fBmuec7rm/jY9KCfvHW2VzJfItVk4t8sw=
 | 
			
		||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
 | 
			
		||||
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4=
 | 
			
		||||
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=
 | 
			
		||||
@@ -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/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/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/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
 | 
			
		||||
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/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
 | 
			
		||||
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.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/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 | 
			
		||||
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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
 | 
			
		||||
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/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
 | 
			
		||||
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/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 | 
			
		||||
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-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/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 | 
			
		||||
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=
 | 
			
		||||
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/ccgo/v4 v4.27.1 h1:emhLB4uoOmkZUnTDFcMI3AbkmU/Evjuerit9Taqe6Ss=
 | 
			
		||||
modernc.org/ccgo/v4 v4.27.1/go.mod h1:543Q0qQhJWekKVS5P6yL5fO6liNhla9Lbm2/B3rEKDE=
 | 
			
		||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
 | 
			
		||||
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/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
 | 
			
		||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
 | 
			
		||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
 | 
			
		||||
modernc.org/libc v1.65.2 h1:drWL1QO9fKXr3kXDN8y+4lKyBr8bA3mtUBQpftq3IJw=
 | 
			
		||||
modernc.org/libc v1.65.2/go.mod h1:VI3V2S5mNka4deJErQ0jsMXe7jgxojE2fOB/mWoHlbc=
 | 
			
		||||
modernc.org/libc v1.65.6 h1:OhJUhmuJ6MVZdqL5qmnd0/my46DKGFhSX4WOR7ijfyE=
 | 
			
		||||
modernc.org/libc v1.65.6/go.mod h1:MOiGAM9lrMBT9L8xT1nO41qYl5eg9gCp9/kWhz5L7WA=
 | 
			
		||||
modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
 | 
			
		||||
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
 | 
			
		||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
 | 
			
		||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
 | 
			
		||||
modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
 | 
			
		||||
modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
 | 
			
		||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
 | 
			
		||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
 | 
			
		||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
 | 
			
		||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
 | 
			
		||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										354
									
								
								backend/main.go
									
									
									
									
									
								
							
							
						
						
									
										354
									
								
								backend/main.go
									
									
									
									
									
								
							@@ -11,6 +11,7 @@ import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-contrib/cors"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -62,20 +63,20 @@ func getUser(c *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user, err := db.GetUser(userId)
 | 
			
		||||
	if errors.Is(err, mysqlite.ErrNoRows) {
 | 
			
		||||
		c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Printf("Error getting user: %v", err)
 | 
			
		||||
		c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if user == nil {
 | 
			
		||||
		c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.IndentedJSON(http.StatusOK, user)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getUserGoals(c *gin.Context) {
 | 
			
		||||
func getUserAllowance(c *gin.Context) {
 | 
			
		||||
	userIdStr := c.Param("userId")
 | 
			
		||||
	userId, err := strconv.Atoi(userIdStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -96,16 +97,59 @@ func getUserGoals(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	goals, err := db.GetUserGoals(userId)
 | 
			
		||||
	allowances, err := db.GetUserAllowances(userId)
 | 
			
		||||
	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})
 | 
			
		||||
		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")
 | 
			
		||||
	userId, err := strconv.Atoi(userIdStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -115,7 +159,7 @@ func createUserGoal(c *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Parse request body
 | 
			
		||||
	var goalRequest CreateGoalRequest
 | 
			
		||||
	var goalRequest CreateAllowanceRequest
 | 
			
		||||
	if err := c.ShouldBindJSON(&goalRequest); err != nil {
 | 
			
		||||
		log.Printf("Error parsing request body: %v", err)
 | 
			
		||||
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
 | 
			
		||||
@@ -124,12 +168,12 @@ func createUserGoal(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
	// Validate request
 | 
			
		||||
	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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Create goal in database
 | 
			
		||||
	goalId, err := db.CreateGoal(userId, &goalRequest)
 | 
			
		||||
	goalId, err := db.CreateAllowance(userId, &goalRequest)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Printf("Error creating goal: %v", err)
 | 
			
		||||
		if err.Error() == "user does not exist" {
 | 
			
		||||
@@ -145,9 +189,8 @@ func createUserGoal(c *gin.Context) {
 | 
			
		||||
	c.IndentedJSON(http.StatusCreated, response)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func deleteUserGoal(c *gin.Context) {
 | 
			
		||||
func bulkPutUserAllowance(c *gin.Context) {
 | 
			
		||||
	userIdStr := c.Param("userId")
 | 
			
		||||
	goalIdStr := c.Param("goalId")
 | 
			
		||||
 | 
			
		||||
	userId, err := strconv.Atoi(userIdStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -156,13 +199,6 @@ func deleteUserGoal(c *gin.Context) {
 | 
			
		||||
		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)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Printf(ErrCheckingUserExist, err)
 | 
			
		||||
@@ -174,18 +210,162 @@ func deleteUserGoal(c *gin.Context) {
 | 
			
		||||
		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.Error() == "goal not found" {
 | 
			
		||||
			c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"})
 | 
			
		||||
		log.Printf("Error updating allowance: %v", err)
 | 
			
		||||
		c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.IndentedJSON(http.StatusOK, gin.H{"message": "Allowance updated successfully"})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func deleteUserAllowance(c *gin.Context) {
 | 
			
		||||
	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 {
 | 
			
		||||
			log.Printf("Error deleting goal: %v", err)
 | 
			
		||||
			log.Printf("Error deleting allowance: %v", err)
 | 
			
		||||
			c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
 | 
			
		||||
		}
 | 
			
		||||
		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) {
 | 
			
		||||
@@ -233,7 +413,7 @@ func getTasks(c *gin.Context) {
 | 
			
		||||
		c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	c.JSON(http.StatusOK, &response)
 | 
			
		||||
	c.IndentedJSON(http.StatusOK, &response)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getTask(c *gin.Context) {
 | 
			
		||||
@@ -289,7 +469,61 @@ func putTask(c *gin.Context) {
 | 
			
		||||
	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")
 | 
			
		||||
	userId, err := strconv.Atoi(userIdStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -298,24 +532,53 @@ func postAllowance(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var allowanceRequest PostAllowance
 | 
			
		||||
	if err := c.ShouldBindJSON(&allowanceRequest); err != nil {
 | 
			
		||||
	var historyRequest PostHistory
 | 
			
		||||
	if err := c.ShouldBindJSON(&historyRequest); err != nil {
 | 
			
		||||
		log.Printf("Error parsing request body: %v", err)
 | 
			
		||||
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = db.AddAllowance(userId, &allowanceRequest)
 | 
			
		||||
	exists, err := db.UserExists(userId)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Printf("Error updating allowance: %v", err)
 | 
			
		||||
		log.Printf(ErrCheckingUserExist, err)
 | 
			
		||||
		c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	c.JSON(http.StatusOK, gin.H{"message": "Allowance updated successfully"})
 | 
			
		||||
	if !exists {
 | 
			
		||||
		c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = db.AddHistory(userId, &historyRequest)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Printf("Error updating history: %v", err)
 | 
			
		||||
		c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	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.
 | 
			
		||||
If the context gets cancelled, the server is shutdown and the database is closed.
 | 
			
		||||
*/
 | 
			
		||||
@@ -324,16 +587,26 @@ func start(ctx context.Context, config *ServerConfig) {
 | 
			
		||||
	defer db.db.MustClose()
 | 
			
		||||
 | 
			
		||||
	router := gin.Default()
 | 
			
		||||
	router.Use(cors.New(cors.Config{
 | 
			
		||||
		AllowOrigins: []string{"*"},
 | 
			
		||||
	}))
 | 
			
		||||
	router.GET("/api/users", getUsers)
 | 
			
		||||
	router.GET("/api/user/:userId", getUser)
 | 
			
		||||
	router.GET("/api/user/:userId/goals", getUserGoals)
 | 
			
		||||
	router.POST("/api/user/:userId/goals", createUserGoal)
 | 
			
		||||
	router.DELETE("/api/user/:userId/goal/:goalId", deleteUserGoal)
 | 
			
		||||
	router.POST("/api/user/:userId/history", postHistory)
 | 
			
		||||
	router.GET("/api/user/:userId/history", getHistory)
 | 
			
		||||
	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.GET("/api/tasks", getTasks)
 | 
			
		||||
	router.GET("/api/task/:taskId", getTask)
 | 
			
		||||
	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{
 | 
			
		||||
		Addr:    config.Addr,
 | 
			
		||||
@@ -366,6 +639,11 @@ func start(ctx context.Context, config *ServerConfig) {
 | 
			
		||||
func main() {
 | 
			
		||||
	config := ServerConfig{
 | 
			
		||||
		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)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,24 +1,26 @@
 | 
			
		||||
create table users
 | 
			
		||||
(
 | 
			
		||||
    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;
 | 
			
		||||
 | 
			
		||||
create table history
 | 
			
		||||
(
 | 
			
		||||
    id integer primary key,
 | 
			
		||||
    user_id integer not null,
 | 
			
		||||
    date date not null,
 | 
			
		||||
    timestamp date not null,
 | 
			
		||||
    amount integer not null
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
create table goals
 | 
			
		||||
create table allowances
 | 
			
		||||
(
 | 
			
		||||
    id integer primary key,
 | 
			
		||||
	user_id integer not null,
 | 
			
		||||
    name text not null,
 | 
			
		||||
    target integer not null,
 | 
			
		||||
    progress integer not null,
 | 
			
		||||
    balance integer not null default 0,
 | 
			
		||||
    weight real not null
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -59,7 +59,33 @@ paths:
 | 
			
		||||
        404:
 | 
			
		||||
          description: The users could not be found.
 | 
			
		||||
 | 
			
		||||
  /user/{userId}/allowance:
 | 
			
		||||
  /user/{userId}/history:
 | 
			
		||||
    get:
 | 
			
		||||
      summary: Gets the allowance history of a user
 | 
			
		||||
      parameters:
 | 
			
		||||
        - in: path
 | 
			
		||||
          name: userId
 | 
			
		||||
          description: The user ID
 | 
			
		||||
          required: true
 | 
			
		||||
          schema:
 | 
			
		||||
            type: integer
 | 
			
		||||
      responses:
 | 
			
		||||
        200:
 | 
			
		||||
          description: Information about the allowance history of the user
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                type: array
 | 
			
		||||
                items:
 | 
			
		||||
                  type: object
 | 
			
		||||
                  properties:
 | 
			
		||||
                    date:
 | 
			
		||||
                      type: string
 | 
			
		||||
                      format: date-time
 | 
			
		||||
                      description: The date of the allowance or expense.
 | 
			
		||||
                    amount:
 | 
			
		||||
                      type: integer
 | 
			
		||||
                      description: The amount of the allowance to be added, in cents. A negative value
 | 
			
		||||
    post:
 | 
			
		||||
      summary: Updates the allowance of a user
 | 
			
		||||
      parameters:
 | 
			
		||||
@@ -88,35 +114,7 @@ paths:
 | 
			
		||||
        400:
 | 
			
		||||
          description: The allowance could not be updated.
 | 
			
		||||
 | 
			
		||||
  /user/{userId}/history:
 | 
			
		||||
    get:
 | 
			
		||||
      summary: Gets the allowance history of a user
 | 
			
		||||
      parameters:
 | 
			
		||||
        - in: path
 | 
			
		||||
          name: userId
 | 
			
		||||
          description: The user ID
 | 
			
		||||
          required: true
 | 
			
		||||
          schema:
 | 
			
		||||
            type: integer
 | 
			
		||||
      responses:
 | 
			
		||||
        200:
 | 
			
		||||
          description: Information about the allowance history of the user
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                type: array
 | 
			
		||||
                items:
 | 
			
		||||
                  type: object
 | 
			
		||||
                  properties:
 | 
			
		||||
                    date:
 | 
			
		||||
                      type: string
 | 
			
		||||
                      format: date-time
 | 
			
		||||
                      description: The date of the allowance or expense.
 | 
			
		||||
                    amount:
 | 
			
		||||
                      type: integer
 | 
			
		||||
                      description: The amount of the allowance to be added, in cents. A negative value
 | 
			
		||||
 | 
			
		||||
  /user/{userId}/goals:
 | 
			
		||||
  /user/{userId}/allowance:
 | 
			
		||||
    get:
 | 
			
		||||
      summary: Gets all goals
 | 
			
		||||
      parameters:
 | 
			
		||||
@@ -203,7 +201,7 @@ paths:
 | 
			
		||||
        404:
 | 
			
		||||
          description: The goals could not be found.
 | 
			
		||||
 | 
			
		||||
  /user/{userId}/goal/{goalId}:
 | 
			
		||||
  /user/{userId}/allowance/{goalId}:
 | 
			
		||||
    get:
 | 
			
		||||
      summary: Gets information about a goal
 | 
			
		||||
      parameters:
 | 
			
		||||
@@ -286,7 +284,7 @@ paths:
 | 
			
		||||
        404:
 | 
			
		||||
          description: The goal could not be found.
 | 
			
		||||
 | 
			
		||||
  /user/{userId}/goal/{goalId}/complete:
 | 
			
		||||
  /user/{userId}/allowance/{goalId}/complete:
 | 
			
		||||
    post:
 | 
			
		||||
      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.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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