Compare commits
	
		
			74 Commits
		
	
	
		
			94a20af04d
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| c9a96f937a | |||
|   | cdbac17215 | ||
|   | ecd43906ce | ||
|   | d6935d2f54 | ||
| 06c8ebcbcc | |||
| 5a20e76df2 | |||
|   | 02c5c6ea68 | ||
|   | 9cbb8756d1 | ||
|   | 604b92b3b3 | ||
|   | c7236394d9 | ||
|   | 720ef83c2e | ||
|   | 5b1d107cac | ||
|   | 662257ebc5 | ||
|   | ad48882bca | ||
|   | 89d31fe150 | ||
|   | 305566c911 | ||
|   | 8c2af22c85 | ||
| a0d0c37fdb | |||
| 2714f550a4 | |||
|   | 344f7a7eef | ||
| 8380e95217 | |||
| db2f518cc2 | |||
|   | 56a19acd0f | ||
|   | 8fa4918743 | ||
|   | 11913d72aa | ||
|   | 45f40a7976 | ||
|   | 63982115a7 | ||
|   | e7b4adfa95 | ||
|   | 550933db11 | ||
|   | daebcdeccd | ||
| 302ceaa629 | |||
| 8cbfff81f6 | |||
| f9fb956efd | |||
| 5a233073c7 | |||
| cd23e72882 | |||
| a82040720a | |||
| a8e3332723 | |||
| f8d1f195de | |||
| 426e456ba7 | |||
|   | 93ec3cbc19 | ||
|   | 5bcbde46ea | ||
|   | f04529067a | ||
|   | 6e07d44733 | ||
|   | 1f21924805 | ||
|   | e85a60ab16 | ||
|   | 61694e340f | ||
|   | f72cc8a802 | ||
| da17f351de | |||
| 79dcfbc02c | |||
| 505faa95a3 | |||
|   | a675d51718 | ||
| 9cb71d53cf | |||
| b5aae3be3d | |||
| 238aedb5c9 | |||
| d1774c1ce0 | |||
| 8fedac21bb | |||
|   | 361baac8f3 | ||
|   | 0007f10ae3 | ||
|   | b48d082edd | ||
|   | bfc1d135de | ||
|   | 0749d8ce7a | ||
|   | ef86deb222 | ||
|   | 6d6460ac3e | ||
| 1589bc9422 | |||
| 790ee3c622 | |||
| 6979368eda | |||
|   | fd14c12a4a | ||
| cc817ed061 | |||
|   | df1b8e4ed7 | ||
| 4355e1b1b7 | |||
|   | 2486bbf1ec | ||
|   | b3e50dadb2 | ||
|   | 572c3c2a41 | ||
|   | 47f43cb0dc | 
							
								
								
									
										24
									
								
								.gitea/workflows/build.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,24 @@ | ||||
| name: Backend Build and Test | ||||
| on: [push] | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: standard-latest | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v4 | ||||
|  | ||||
|       - name: Setup Go | ||||
|         uses: actions/setup-go@v5 | ||||
|         with: | ||||
|           go-version: '>=1.24' | ||||
|  | ||||
|       - name: Build | ||||
|         run: | | ||||
|           cd backend | ||||
|           go build . | ||||
|  | ||||
|       - name: Test | ||||
|         run: | | ||||
|           cd backend | ||||
|           go test . -v | ||||
							
								
								
									
										27
									
								
								.gitea/workflows/deploy.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,27 @@ | ||||
| name: Backend Deploy | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: standard-latest | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v4 | ||||
|  | ||||
|       - name: Login | ||||
|         with: | ||||
|           package_rw: ${{ secrets.PACKAGE_RW }} | ||||
|         run: docker login gitea.seeseepuff.be -u seeseemelk -p ${{ secrets.PACKAGE_RW }} | ||||
|  | ||||
|       - name: Build | ||||
|         run: | | ||||
|           cd backend | ||||
|           docker build -t gitea.seeseepuff.be/seeseemelk/allowance-planner:$(git rev-parse --short HEAD) . | ||||
|  | ||||
|       - name: Push | ||||
|         run: | | ||||
|           cd backend | ||||
|           docker push gitea.seeseepuff.be/seeseemelk/allowance-planner:$(git rev-parse --short HEAD) | ||||
							
								
								
									
										32
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,2 +1,32 @@ | ||||
| # Allowance Planner 2000 | ||||
| An improved Allowance Planner app. | ||||
| An improved Allowance Planner app. | ||||
|  | ||||
| ## Running backend | ||||
| In order to run the backend, go to the `backend` directory and run: | ||||
|  | ||||
| ```bash | ||||
| $ go run . | ||||
| ``` | ||||
|  | ||||
| ## Running frontend | ||||
| In order to run the frontend, go to the `allowance-planner-v2` directory in the `frontend` directory and run: | ||||
|  | ||||
| ```bash | ||||
| $ ionic serve | ||||
| ``` | ||||
|  | ||||
| ## Running frontend | ||||
| In order to build the frontend for android, go to the `allowance-planner-v2` directory in the `frontend` directory and run: | ||||
|  | ||||
| ```bash | ||||
| $ ionic capacitor build android | ||||
| ``` | ||||
|  | ||||
| ## Backend links | ||||
|  | ||||
| ```bash | ||||
| Main: https://allowanceplanner.seeseepuff.be/api | ||||
| ``` | ||||
| ```bash | ||||
| Test: http://localhost:8080/api | ||||
| ``` | ||||
|   | ||||
							
								
								
									
										2
									
								
								backend/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,2 +1,4 @@ | ||||
| *.db3 | ||||
| *.db3-* | ||||
| *.db3.* | ||||
| /allowance_planner | ||||
|   | ||||
							
								
								
									
										14
									
								
								backend/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | ||||
| FROM golang:1.24.2-alpine3.21 | ||||
|  | ||||
| WORKDIR /app | ||||
| COPY go.mod go.sum ./ | ||||
| RUN go mod download | ||||
|  | ||||
| COPY migrations ./migrations/ | ||||
| COPY *.go ./ | ||||
| COPY *.gohtml ./ | ||||
| RUN go build -o /allowance_planner | ||||
|  | ||||
| EXPOSE 8080 | ||||
| ENV GIN_MODE=release | ||||
| CMD ["/allowance_planner"] | ||||
| @@ -2,20 +2,23 @@ package main | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"github.com/gavv/httpexpect/v2" | ||||
| 	"strconv" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gavv/httpexpect/v2" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	TestGoalName = "Test Goal" | ||||
| 	TestHistoryName = "Test History" | ||||
| ) | ||||
|  | ||||
| func startServer(t *testing.T) *httpexpect.Expect { | ||||
| 	config := ServerConfig{ | ||||
| 		Datasource: ":memory:", | ||||
| 		Addr:       ":0", | ||||
| 		Started:    make(chan bool), | ||||
| 		//Datasource: "test.db", | ||||
| 		Addr:    ":0", | ||||
| 		Started: make(chan bool), | ||||
| 	} | ||||
| 	go start(t.Context(), &config) | ||||
| 	<-config.Started | ||||
| @@ -35,6 +38,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 +51,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":   TestHistoryName, | ||||
| 		"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(TestHistoryName) | ||||
| 	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":   TestHistoryName, | ||||
| 		"target": 5000, | ||||
| 		"weight": 10, | ||||
| 	} | ||||
|  | ||||
| 	response := e.POST("/user/1/goals"). | ||||
| 	response := e.POST("/user/1/allowance"). | ||||
| 		WithJSON(requestBody). | ||||
| 		Expect(). | ||||
| 		Status(201). | ||||
| @@ -104,40 +110,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(TestHistoryName) | ||||
| 	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":   TestHistoryName, | ||||
| 		"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 +153,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 +163,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":   TestHistoryName, | ||||
| 		"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":   TestHistoryName, | ||||
| 		"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 +257,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 +283,85 @@ 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 TestCreateScheduleTask(t *testing.T) { | ||||
| //	e := startServer(t) | ||||
| // | ||||
| //	// Create a new task without assigned user | ||||
| //	requestBody := map[string]interface{}{ | ||||
| //		"name":     "Test Task", | ||||
| //		"reward":   100, | ||||
| //		"schedule": "0 */5 * * * *", | ||||
| //	} | ||||
| // | ||||
| //	response := e.POST("/tasks"). | ||||
| //		WithJSON(requestBody). | ||||
| //		Expect(). | ||||
| //		Status(201). // Expect Created status | ||||
| //		JSON().Object() | ||||
| // | ||||
| //	requestBody["schedule"] = "every 5 seconds" | ||||
| //	e.POST("/tasks").WithJSON(requestBody).Expect().Status(400) | ||||
| // | ||||
| //	// Verify the response has an ID | ||||
| //	response.ContainsKey("id") | ||||
| //	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("schedule").IsEqual("0 */5 * * * *") | ||||
| //	result.Value("reward").IsEqual(100) | ||||
| //	result.Value("assigned").IsNull() | ||||
| // | ||||
| //	// Complete the task | ||||
| //	e.POST("/task/1/complete").Expect().Status(200) | ||||
| // | ||||
| //	// Set expires date to 1 second in the past | ||||
| //	db.db.Query("update tasks set next_run = ? where id = 1").Bind(time.Now().Add(10 * -time.Minute).Unix()).MustExec() | ||||
| // | ||||
| //	// Verify a new task is created | ||||
| //	newTask := e.GET("/task/2").Expect().Status(200).JSON().Object() | ||||
| //	newTask.Value("id").IsEqual(2) | ||||
| //	newTask.Value("name").IsEqual("Test Task") | ||||
| //	newTask.Value("schedule").IsEqual("0 */5 * * * *") | ||||
| //	newTask.Value("reward").IsEqual(100) | ||||
| //	newTask.Value("assigned").IsNull() | ||||
| //} | ||||
|  | ||||
| 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 +411,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) | ||||
|  | ||||
| @@ -386,21 +484,568 @@ func TestPutTaskInvalidTaskId(t *testing.T) { | ||||
| 	e.PUT("/task/999").WithJSON(requestBody).Expect().Status(404) | ||||
| } | ||||
|  | ||||
| func TestPostAllowance(t *testing.T) { | ||||
| func TestPostHistory(t *testing.T) { | ||||
| 	e := startServer(t) | ||||
|  | ||||
| 	e.POST("/user/1/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, Description: "Add a 100"}).Expect().Status(200) | ||||
| 	e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 20, Description: "Lolol"}).Expect().Status(200) | ||||
| 	e.POST("/user/1/history").WithJSON(PostHistory{Allowance: -10, Description: "Subtracting"}).Expect().Status(200) | ||||
|  | ||||
| 	response := e.GET("/user/1").Expect().Status(200).JSON().Object() | ||||
| 	response.Value("allowance").Number().IsEqual(100 + 20 - 10) | ||||
| } | ||||
|  | ||||
| func TestPostAllowanceInvalidUserId(t *testing.T) { | ||||
| func TestPostHistoryInvalidUserId(t *testing.T) { | ||||
| 	e := startServer(t) | ||||
|  | ||||
| 	e.POST("/user/999/allowance").WithJSON(PostAllowance{Allowance: 100}).Expect(). | ||||
| 	e.POST("/user/999/history").WithJSON(PostHistory{Allowance: 100, Description: "Good"}).Expect(). | ||||
| 		Status(404) | ||||
|  | ||||
| } | ||||
|  | ||||
| func TestPostHistoryInvalidDescription(t *testing.T) { | ||||
| 	e := startServer(t) | ||||
|  | ||||
| 	e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 100}).Expect(). | ||||
| 		Status(400) | ||||
| } | ||||
|  | ||||
| func TestGetHistory(t *testing.T) { | ||||
| 	e := startServer(t) | ||||
|  | ||||
| 	e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 100, Description: "Add 100"}).Expect().Status(200) | ||||
| 	e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 20, Description: "Add 20"}).Expect().Status(200) | ||||
| 	e.POST("/user/1/history").WithJSON(PostHistory{Allowance: -10, Description: "Subtract 10"}).Expect().Status(200) | ||||
|  | ||||
| 	response := e.GET("/user/1/history").Expect().Status(200).JSON().Array() | ||||
| 	response.Length().IsEqual(3) | ||||
| 	response.Value(0).Object().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(0).Object().Value("description").String().IsEqual("Add 100") | ||||
|  | ||||
| 	response.Value(1).Object().Value("allowance").Number().IsEqual(20) | ||||
| 	response.Value(1).Object().Value("description").String().IsEqual("Add 20") | ||||
|  | ||||
| 	response.Value(2).Object().Value("allowance").Number().IsEqual(-10) | ||||
| 	response.Value(2).Object().Value("description").String().IsEqual("Subtract 10") | ||||
| } | ||||
|  | ||||
| func TestGetUserAllowanceById(t *testing.T) { | ||||
| 	e := startServer(t) | ||||
|  | ||||
| 	// Create a new allowance | ||||
| 	requestBody := map[string]interface{}{ | ||||
| 		"name":   TestHistoryName, | ||||
| 		"target": 5000, | ||||
| 		"weight": 10, | ||||
| 		"colour": "#FF5733", | ||||
| 	} | ||||
| 	resp := e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201).JSON().Object() | ||||
| 	allowanceId := int(resp.Value("id").Number().Raw()) | ||||
|  | ||||
| 	// 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(TestHistoryName) | ||||
| 	result.Value("target").IsEqual(5000) | ||||
| 	result.Value("weight").IsEqual(10) | ||||
| 	result.Value("progress").IsEqual(0) | ||||
| 	result.Value("colour").IsEqual("#FF5733") | ||||
|  | ||||
| 	resultArray := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array() | ||||
| 	resultArray.Length().IsEqual(2) | ||||
| 	result = resultArray.Value(1).Object() | ||||
| 	result.Value("id").IsEqual(allowanceId) | ||||
| 	result.Value("name").IsEqual(TestHistoryName) | ||||
| 	result.Value("target").IsEqual(5000) | ||||
| 	result.Value("weight").IsEqual(10) | ||||
| 	result.Value("progress").IsEqual(0) | ||||
| 	result.Value("colour").IsEqual("#FF5733") | ||||
| } | ||||
|  | ||||
| func TestGetUserByAllowanceIdInvalidAllowance(t *testing.T) { | ||||
| 	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":   TestHistoryName, | ||||
| 		"target": 5000, | ||||
| 		"weight": 10, | ||||
| 		"colour": "#FF5733", | ||||
| 	} | ||||
| 	resp := e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201).JSON().Object() | ||||
| 	allowanceId := int(resp.Value("id").Number().Raw()) | ||||
|  | ||||
| 	// Update the allowance | ||||
| 	updateRequest := map[string]interface{}{ | ||||
| 		"name":   "Updated Allowance", | ||||
| 		"target": 6000, | ||||
| 		"weight": 15, | ||||
| 		"colour": "#3357FF", | ||||
| 	} | ||||
| 	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) | ||||
| 	result.Value("colour").IsEqual("#3357FF") | ||||
| } | ||||
|  | ||||
| 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: 100, | ||||
| 		Weight: 50, | ||||
| 	}).Expect().Status(201) | ||||
| 	e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{ | ||||
| 		Name:   "Test Allowance 1", | ||||
| 		Target: 10, | ||||
| 		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().InDelta(30.34, 0.01) | ||||
| 	allowances.Value(1).Object().Value("id").Number().IsEqual(1) | ||||
| 	allowances.Value(1).Object().Value("progress").Number().InDelta(60.66, 0.01) | ||||
| 	allowances.Value(2).Object().Value("id").Number().IsEqual(2) | ||||
| 	allowances.Value(2).Object().Value("progress").Number().IsEqual(10) | ||||
|  | ||||
| 	// 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 TestCompleteTaskWithNoWeights(t *testing.T) { | ||||
| 	e := startServer(t) | ||||
| 	taskId := createTestTaskWithAmount(e, 101) | ||||
|  | ||||
| 	e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1) | ||||
|  | ||||
| 	// Ensure main allowance has no weight | ||||
| 	e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{ | ||||
| 		Weight: 0, | ||||
| 	}).Expect().Status(200) | ||||
|  | ||||
| 	// Complete the task | ||||
| 	e.POST("/task/" + strconv.Itoa(taskId) + "/complete").Expect().Status(200) | ||||
|  | ||||
| 	// Verify the task is marked as completed | ||||
| 	e.GET("/task/" + strconv.Itoa(taskId)).Expect().Status(404) | ||||
|  | ||||
| 	// Verify the allowances are updated for user 1 | ||||
| 	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array() | ||||
| 	allowances.Length().IsEqual(1) | ||||
| 	allowances.Value(0).Object().Value("id").Number().IsEqual(0) | ||||
| 	allowances.Value(0).Object().Value("progress").Number().InDelta(101.00, 0.01) | ||||
|  | ||||
| 	// And also for user 2 | ||||
| 	allowances = e.GET("/user/2/allowance").Expect().Status(200).JSON().Array() | ||||
| 	allowances.Length().IsEqual(1) | ||||
| 	allowances.Value(0).Object().Value("id").Number().IsEqual(0) | ||||
| 	allowances.Value(0).Object().Value("progress").Number().InDelta(101.00, 0.01) | ||||
| } | ||||
|  | ||||
| func TestCompleteTaskAllowanceWeightsSumTo0(t *testing.T) { | ||||
| 	e := startServer(t) | ||||
| 	taskId := createTestTaskWithAmount(e, 101) | ||||
|  | ||||
| 	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) | ||||
|  | ||||
| 	// Update base allowance | ||||
| 	e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{ | ||||
| 		Weight: 0, | ||||
| 	}).Expect().Status(200) | ||||
|  | ||||
| 	// Complete the task | ||||
| 	e.POST("/task/1/complete").Expect().Status(200) | ||||
|  | ||||
| 	// 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().Length().IsEqual(3) | ||||
| 	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(0).Object().Value("description").String().IsEqual("Task completed: Test Task") | ||||
|  | ||||
| 	history.Value(1).Object().Length().IsEqual(3) | ||||
| 	history.Value(1).Object().Value("allowance").Number().IsEqual(-100) | ||||
| 	history.Value(1).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0)) | ||||
| 	history.Value(1).Object().Value("description").String().IsEqual("Allowance completed: Test Allowance 1") | ||||
| } | ||||
|  | ||||
| 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 TestAddAllowanceSimple(t *testing.T) { | ||||
| 	e := startServer(t) | ||||
|  | ||||
| 	createTestAllowance(e, "Test Allowance 1", 1000, 1) | ||||
|  | ||||
| 	request := map[string]interface{}{ | ||||
| 		"amount":      10, | ||||
| 		"description": "Added to allowance 1", | ||||
| 	} | ||||
| 	e.POST("/user/1/allowance/1/add").WithJSON(request).Expect().Status(200) | ||||
|  | ||||
| 	// Verify the allowance is updated | ||||
| 	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array() | ||||
| 	allowances.Value(1).Object().Value("id").Number().IsEqual(1) | ||||
| 	allowances.Value(1).Object().Value("progress").Number().InDelta(10.0, 0.01) | ||||
|  | ||||
| 	// Verify the history is updated | ||||
| 	history := e.GET("/user/1/history").Expect().Status(200).JSON().Array() | ||||
| 	history.Length().IsEqual(1) | ||||
| 	history.Value(0).Object().Value("allowance").Number().InDelta(10.0, 0.01) | ||||
| 	history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0)) | ||||
| 	history.Value(0).Object().Value("description").String().IsEqual("Added to allowance 1") | ||||
| } | ||||
|  | ||||
| func TestAddAllowanceWithSpillage(t *testing.T) { | ||||
| 	e := startServer(t) | ||||
|  | ||||
| 	createTestAllowance(e, "Test Allowance 1", 5, 1) | ||||
| 	createTestAllowance(e, "Test Allowance 2", 5, 1) | ||||
| 	e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{Weight: 1}).Expect().Status(200) | ||||
|  | ||||
| 	request := map[string]interface{}{ | ||||
| 		"amount":      10, | ||||
| 		"description": "Added to allowance 1", | ||||
| 	} | ||||
| 	e.POST("/user/1/allowance/1/add").WithJSON(request).Expect().Status(200) | ||||
|  | ||||
| 	// Verify the allowance is updated | ||||
| 	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array() | ||||
| 	allowances.Value(1).Object().Value("id").Number().IsEqual(1) | ||||
| 	allowances.Value(1).Object().Value("progress").Number().InDelta(5.0, 0.01) | ||||
|  | ||||
| 	allowances.Value(2).Object().Value("id").Number().IsEqual(2) | ||||
| 	allowances.Value(2).Object().Value("progress").Number().InDelta(2.5, 0.01) | ||||
|  | ||||
| 	allowances.Value(0).Object().Value("id").Number().IsEqual(0) | ||||
| 	allowances.Value(0).Object().Value("progress").Number().InDelta(2.5, 0.01) | ||||
|  | ||||
| 	// Verify the history is updated | ||||
| 	history := e.GET("/user/1/history").Expect().Status(200).JSON().Array() | ||||
| 	history.Length().IsEqual(1) | ||||
| 	history.Value(0).Object().Value("allowance").Number().InDelta(10.0, 0.01) | ||||
| 	history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0)) | ||||
| 	history.Value(0).Object().Value("description").String().IsEqual("Added to allowance 1") | ||||
| } | ||||
|  | ||||
| func TestAddAllowanceIdZero(t *testing.T) { | ||||
| 	e := startServer(t) | ||||
|  | ||||
| 	createTestAllowance(e, "Test Allowance 1", 1000, 1) | ||||
|  | ||||
| 	request := map[string]interface{}{ | ||||
| 		"amount":      10, | ||||
| 		"description": "Added to allowance 1", | ||||
| 	} | ||||
| 	e.POST("/user/1/allowance/0/add").WithJSON(request).Expect().Status(200) | ||||
|  | ||||
| 	// Verify the allowance is updated | ||||
| 	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array() | ||||
| 	allowances.Value(0).Object().Value("id").Number().IsEqual(0) | ||||
| 	allowances.Value(0).Object().Value("progress").Number().InDelta(10.0, 0.01) | ||||
|  | ||||
| 	// Verify the history is updated | ||||
| 	history := e.GET("/user/1/history").Expect().Status(200).JSON().Array() | ||||
| 	history.Length().IsEqual(1) | ||||
| 	history.Value(0).Object().Value("allowance").Number().InDelta(10.0, 0.01) | ||||
| 	history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0)) | ||||
| 	history.Value(0).Object().Value("description").String().IsEqual("Added to allowance 1") | ||||
| } | ||||
|  | ||||
| func TestSubtractAllowanceSimple(t *testing.T) { | ||||
| 	e := startServer(t) | ||||
|  | ||||
| 	createTestAllowance(e, "Test Allowance 1", 1000, 1) | ||||
|  | ||||
| 	request := map[string]interface{}{ | ||||
| 		"amount":      10, | ||||
| 		"description": "Added to allowance 1", | ||||
| 	} | ||||
| 	e.POST("/user/1/allowance/1/add").WithJSON(request).Expect().Status(200) | ||||
| 	request["amount"] = -2.5 | ||||
| 	e.POST("/user/1/allowance/1/add").WithJSON(request).Expect().Status(200) | ||||
|  | ||||
| 	// Verify the allowance is updated | ||||
| 	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array() | ||||
| 	allowances.Value(1).Object().Value("id").Number().IsEqual(1) | ||||
| 	allowances.Value(1).Object().Value("progress").Number().InDelta(7.5, 0.01) | ||||
|  | ||||
| 	// Verify the 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().InDelta(10.0, 0.01) | ||||
| 	history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0)) | ||||
| 	history.Value(0).Object().Value("description").String().IsEqual("Added to allowance 1") | ||||
|  | ||||
| 	history.Value(1).Object().Value("allowance").Number().InDelta(-2.5, 0.01) | ||||
| 	history.Value(1).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0)) | ||||
| 	history.Value(1).Object().Value("description").String().IsEqual("Added to allowance 1") | ||||
| } | ||||
|  | ||||
| func TestSubtractllowanceIdZero(t *testing.T) { | ||||
| 	e := startServer(t) | ||||
|  | ||||
| 	createTestAllowance(e, "Test Allowance 1", 1000, 1) | ||||
|  | ||||
| 	request := map[string]interface{}{ | ||||
| 		"amount":      10, | ||||
| 		"description": "Added to allowance 1", | ||||
| 	} | ||||
| 	e.POST("/user/1/allowance/0/add").WithJSON(request).Expect().Status(200) | ||||
| 	request["amount"] = -2.5 | ||||
| 	e.POST("/user/1/allowance/0/add").WithJSON(request).Expect().Status(200) | ||||
|  | ||||
| 	// Verify the allowance is updated | ||||
| 	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array() | ||||
| 	allowances.Value(0).Object().Value("id").Number().IsEqual(0) | ||||
| 	allowances.Value(0).Object().Value("progress").Number().InDelta(7.5, 0.01) | ||||
|  | ||||
| 	// Verify the 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().InDelta(10.0, 0.01) | ||||
| 	history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0)) | ||||
| 	history.Value(0).Object().Value("description").String().IsEqual("Added to allowance 1") | ||||
|  | ||||
| 	history.Value(1).Object().Value("allowance").Number().InDelta(-2.5, 0.01) | ||||
| 	history.Value(1).Object().Value("description").String().IsEqual("Added to allowance 1") | ||||
| } | ||||
|  | ||||
| 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 float64, 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) | ||||
| } | ||||
|  | ||||
| // Transfer tests | ||||
| func TestTransferSuccessful(t *testing.T) { | ||||
| 	e := startServer(t) | ||||
|  | ||||
| 	// Create two allowances for user 1 | ||||
| 	createTestAllowance(e, "From Allowance", 100, 1) | ||||
| 	createTestAllowance(e, "To Allowance", 100, 1) | ||||
|  | ||||
| 	// Add 30 to allowance 1 | ||||
| 	req := map[string]interface{}{"amount": 30, "description": "funds"} | ||||
| 	e.POST("/user/1/allowance/1/add").WithJSON(req).Expect().Status(200) | ||||
|  | ||||
| 	// Transfer 10 from 1 to 2 | ||||
| 	transfer := map[string]interface{}{"from": 1, "to": 2, "amount": 10} | ||||
| 	e.POST("/transfer").WithJSON(transfer).Expect().Status(200).JSON().Object().Value("message").IsEqual("Transfer successful") | ||||
|  | ||||
| 	// Verify balances | ||||
| 	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array() | ||||
| 	allowances.Value(1).Object().Value("progress").Number().InDelta(20.0, 0.01) | ||||
| 	allowances.Value(2).Object().Value("progress").Number().InDelta(10.0, 0.01) | ||||
| } | ||||
|  | ||||
| func TestTransferCapsAtTarget(t *testing.T) { | ||||
| 	e := startServer(t) | ||||
|  | ||||
| 	// Create two allowances | ||||
| 	createTestAllowance(e, "From Allowance", 100, 1) | ||||
| 	createTestAllowance(e, "To Allowance", 5, 1) | ||||
|  | ||||
| 	// Add 10 to allowance 1 | ||||
| 	req := map[string]interface{}{"amount": 10, "description": "funds"} | ||||
| 	e.POST("/user/1/allowance/1/add").WithJSON(req).Expect().Status(200) | ||||
|  | ||||
| 	// Transfer 10 from 1 to 2, but to only needs 5 | ||||
| 	transfer := map[string]interface{}{"from": 1, "to": 2, "amount": 10} | ||||
| 	e.POST("/transfer").WithJSON(transfer).Expect().Status(200) | ||||
|  | ||||
| 	// Verify capped transfer | ||||
| 	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array() | ||||
| 	allowances.Value(1).Object().Value("progress").Number().InDelta(5.0, 0.01) // from had 10, transferred 5 -> left 5 | ||||
| 	allowances.Value(2).Object().Value("progress").Number().InDelta(5.0, 0.01) // to reached target | ||||
| } | ||||
|  | ||||
| func TestTransferDifferentUsersFails(t *testing.T) { | ||||
| 	e := startServer(t) | ||||
|  | ||||
| 	// Create allowance for user 1 and user 2 | ||||
| 	createTestAllowance(e, "User1 Allowance", 100, 1) | ||||
| 	// create for user 2 | ||||
| 	e.POST("/user/2/allowance").WithJSON(CreateAllowanceRequest{Name: "User2 Allowance", Target: 100, Weight: 1}).Expect().Status(201) | ||||
|  | ||||
| 	// Add to user1 allowance | ||||
| 	req := map[string]interface{}{"amount": 10, "description": "funds"} | ||||
| 	e.POST("/user/1/allowance/1/add").WithJSON(req).Expect().Status(200) | ||||
|  | ||||
| 	// Attempt transfer between different users | ||||
| 	transfer := map[string]interface{}{"from": 1, "to": 1 /* wrong id to simulate different user's id? */} | ||||
| 	// To ensure different user, fetch the allowance id for user2 (it's 1 for user2 in its own context but global id will be 2) | ||||
| 	// Create above for user2 produced global id 2, so use that | ||||
| 	transfer = map[string]interface{}{"from": 1, "to": 2, "amount": 5} | ||||
| 	e.POST("/transfer").WithJSON(transfer).Expect().Status(400) | ||||
| } | ||||
|  | ||||
| func TestTransferInsufficientFunds(t *testing.T) { | ||||
| 	e := startServer(t) | ||||
|  | ||||
| 	// Create two allowances | ||||
| 	createTestAllowance(e, "From Allowance", 100, 1) | ||||
| 	createTestAllowance(e, "To Allowance", 100, 1) | ||||
|  | ||||
| 	// Ensure from has 0 balance | ||||
| 	transfer := map[string]interface{}{"from": 1, "to": 2, "amount": 10} | ||||
| 	resp := e.POST("/transfer").WithJSON(transfer).Expect().Status(400).JSON().Object() | ||||
| 	// Error text should mention insufficient funds | ||||
| 	resp.Value("error").String().ContainsFold("insufficient") | ||||
| } | ||||
|  | ||||
| func TestTransferNotFound(t *testing.T) { | ||||
| 	e := startServer(t) | ||||
|  | ||||
| 	// No allowances exist yet (only user rows). Attempt transfer with non-existent IDs | ||||
| 	transfer := map[string]interface{}{"from": 999, "to": 1000, "amount": 1} | ||||
| 	e.POST("/transfer").WithJSON(transfer).Expect().Status(404) | ||||
| } | ||||
|   | ||||
							
								
								
									
										34
									
								
								backend/colour.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,34 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| ) | ||||
|  | ||||
| func ConvertStringToColour(colourStr string) (int, error) { | ||||
| 	if len(colourStr) == 0 { | ||||
| 		return 0xFF0000, nil // Default colour if no string is provided | ||||
| 	} | ||||
| 	if colourStr[0] == '#' { | ||||
| 		colourStr = colourStr[1:] | ||||
| 	} | ||||
| 	if len(colourStr) != 6 && len(colourStr) != 3 { | ||||
| 		return 0, errors.New("colour must be a valid hex string") | ||||
| 	} | ||||
| 	var colour int | ||||
| 	_, err := fmt.Sscanf(colourStr, "%x", &colour) | ||||
| 	if err != nil { | ||||
| 		return 0, fmt.Errorf("invalid colour format: %v", err) | ||||
| 	} | ||||
| 	if len(colourStr) == 3 { | ||||
| 		r := (colour & 0xF00) >> 8 | ||||
| 		g := (colour & 0x0F0) >> 4 | ||||
| 		b := (colour & 0x00F) >> 0 | ||||
| 		colour = (r << 16 << 4) | (g << 8 << 4) | (b << 0 << 4) | ||||
| 	} | ||||
| 	return colour, nil | ||||
| } | ||||
|  | ||||
| func ConvertColourToString(colour int) string { | ||||
| 	return fmt.Sprintf("#%06X", colour) | ||||
| } | ||||
							
								
								
									
										30
									
								
								backend/colour_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,30 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"github.com/stretchr/testify/require" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestConvertStringToColourWithSign(t *testing.T) { | ||||
| 	colour, err := ConvertStringToColour("#123456") | ||||
| 	require.NoError(t, err) | ||||
| 	require.Equal(t, 0x123456, colour) | ||||
| } | ||||
|  | ||||
| func TestConvertStringToColourWithoutSign(t *testing.T) { | ||||
| 	colour, err := ConvertStringToColour("123456") | ||||
| 	require.NoError(t, err) | ||||
| 	require.Equal(t, 0x123456, colour) | ||||
| } | ||||
|  | ||||
| func TestConvertStringToColourWithSignThreeDigits(t *testing.T) { | ||||
| 	colour, err := ConvertStringToColour("#ABC") | ||||
| 	require.NoError(t, err) | ||||
| 	require.Equal(t, 0xA0B0C0, colour) | ||||
| } | ||||
|  | ||||
| func TestConvertStringToColourWithoutSignThreeDigits(t *testing.T) { | ||||
| 	colour, err := ConvertStringToColour("ABC") | ||||
| 	require.NoError(t, err) | ||||
| 	require.Equal(t, 0xA0B0C0, colour) | ||||
| } | ||||
							
								
								
									
										596
									
								
								backend/db.go
									
									
									
									
									
								
							
							
						
						| @@ -2,7 +2,10 @@ package main | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/adhocore/gronx" | ||||
| 	"log" | ||||
| 	"math" | ||||
| 	"time" | ||||
|  | ||||
| 	"gitea.seeseepuff.be/seeseemelk/mysqlite" | ||||
| @@ -49,8 +52,10 @@ func (db *Db) GetUsers() ([]User, error) { | ||||
| func (db *Db) GetUser(id int) (*UserWithAllowance, error) { | ||||
| 	user := &UserWithAllowance{} | ||||
|  | ||||
| 	var allowance int | ||||
| 	err := db.db.Query("select u.id, u.name, (select ifnull(sum(h.amount), 0) from history h where h.user_id = u.id) from users u where u.id = ?"). | ||||
| 		Bind(id).ScanSingle(&user.ID, &user.Name, &user.Allowance) | ||||
| 		Bind(id).ScanSingle(&user.ID, &user.Name, &allowance) | ||||
| 	user.Allowance = float64(allowance) / 100.0 | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| @@ -67,27 +72,66 @@ 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 | ||||
| 	var progress int64 | ||||
|  | ||||
| 	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(&progress, &totalAllowance.Weight) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	totalAllowance.Progress = float64(progress) / 100.0 | ||||
| 	allowances = append(allowances, totalAllowance) | ||||
|  | ||||
| 	for row := range db.db.Query("select id, name, target, balance, weight, colour 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{} | ||||
| 		var target, progress, colour int | ||||
| 		err = row.Scan(&allowance.ID, &allowance.Name, &target, &progress, &allowance.Weight, &colour) | ||||
| 		allowance.Target = float64(target) / 100.0 | ||||
| 		allowance.Progress = float64(progress) / 100.0 | ||||
| 		allowance.Colour = ConvertColourToString(colour) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		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 { | ||||
| 		var progress int64 | ||||
| 		err := db.db.Query("select balance, weight from users where id = ?"). | ||||
| 			Bind(userId).ScanSingle(&progress, &allowance.Weight) | ||||
| 		allowance.Progress = float64(progress) / 100.0 | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} else { | ||||
| 		var target, progress int64 | ||||
| 		var colour int | ||||
| 		err := db.db.Query("select id, name, target, balance, weight, colour from allowances where user_id = ? and id = ?"). | ||||
| 			Bind(userId, allowanceId). | ||||
| 			ScanSingle(&allowance.ID, &allowance.Name, &target, &progress, &allowance.Weight, &colour) | ||||
| 		allowance.Target = float64(target) / 100.0 | ||||
| 		allowance.Progress = float64(progress) / 100.0 | ||||
| 		allowance.Colour = ConvertColourToString(colour) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
| 	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 | ||||
| @@ -102,9 +146,15 @@ 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). | ||||
| 	// Convert string colour to a valid hex format | ||||
| 	colour, err := ConvertStringToColour(allowance.Colour) | ||||
| 	if err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
|  | ||||
| 	// Insert the new allowance | ||||
| 	err = tx.Query("insert into allowances (user_id, name, target, weight, colour) values (?, ?, ?, ?, ?)"). | ||||
| 		Bind(userId, allowance.Name, int(math.Round(allowance.Target*100.0)), allowance.Weight, colour). | ||||
| 		Exec() | ||||
|  | ||||
| 	if err != nil { | ||||
| @@ -127,21 +177,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 | ||||
| 	} | ||||
| @@ -149,6 +199,114 @@ 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 | ||||
| 	var allowanceName string | ||||
| 	err = tx.Query("select balance, name from allowances where id = ? and user_id = ?"). | ||||
| 		Bind(allowanceId, userId).ScanSingle(&cost, &allowanceName) | ||||
| 	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, description) values (?, ?, ?, ?)"). | ||||
| 		Bind(userId, time.Now().Unix(), -cost, fmt.Sprintf("Allowance completed: %s", allowanceName)). | ||||
| 		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() | ||||
|  | ||||
| 	colour, err := ConvertStringToColour(allowance.Colour) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	target := int(math.Round(allowance.Target * 100.0)) | ||||
| 	err = tx.Query("update allowances set name=?, target=?, weight=?, colour=? where id = ? and user_id = ?"). | ||||
| 		Bind(allowance.Name, target, allowance.Weight, colour, allowanceId, userId). | ||||
| 		Exec() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	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 { | ||||
| @@ -156,9 +314,20 @@ func (db *Db) CreateTask(task *CreateTaskRequest) (int, error) { | ||||
| 	} | ||||
| 	defer tx.MustRollback() | ||||
|  | ||||
| 	var nextRun *int64 | ||||
| 	if task.Schedule != nil { | ||||
| 		nextRunTime, err := gronx.NextTick(*task.Schedule, false) | ||||
| 		if err != nil { | ||||
| 			return 0, fmt.Errorf("failed to calculate next run: %w", err) | ||||
| 		} | ||||
| 		nextRunTimeAsInt := nextRunTime.Unix() | ||||
| 		nextRun = &nextRunTimeAsInt | ||||
| 	} | ||||
|  | ||||
| 	// Insert the new task | ||||
| 	err = tx.Query("insert into tasks (name, reward, assigned) values (?, ?, ?)"). | ||||
| 		Bind(task.Name, task.Reward, task.Assigned). | ||||
| 	reward := int(math.Round(task.Reward * 100.0)) | ||||
| 	err = tx.Query("insert into tasks (name, reward, assigned, schedule, next_run) values (?, ?, ?, ?, ?)"). | ||||
| 		Bind(task.Name, reward, task.Assigned, task.Schedule, nextRun). | ||||
| 		Exec() | ||||
|  | ||||
| 	if err != nil { | ||||
| @@ -182,12 +351,18 @@ func (db *Db) CreateTask(task *CreateTaskRequest) (int, error) { | ||||
| } | ||||
|  | ||||
| func (db *Db) GetTasks() ([]Task, error) { | ||||
| 	tasks := make([]Task, 0) | ||||
| 	var err error | ||||
| 	err := db.UpdateScheduledTasks() | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to update scheduled tasks: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	for row := range db.db.Query("select id, name, reward, assigned from tasks").Range(&err) { | ||||
| 	tasks := make([]Task, 0) | ||||
|  | ||||
| 	for row := range db.db.Query("select id, name, reward, assigned, schedule from tasks where completed is null").Range(&err) { | ||||
| 		task := Task{} | ||||
| 		err = row.Scan(&task.ID, &task.Name, &task.Reward, &task.Assigned) | ||||
| 		var reward int64 | ||||
| 		err = row.Scan(&task.ID, &task.Name, &reward, &task.Assigned, &task.Schedule) | ||||
| 		task.Reward = float64(reward) / 100.0 | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| @@ -202,14 +377,93 @@ func (db *Db) GetTasks() ([]Task, error) { | ||||
| func (db *Db) GetTask(id int) (Task, error) { | ||||
| 	task := Task{} | ||||
|  | ||||
| 	err := db.db.Query("select id, name, reward, assigned from tasks where id = ?"). | ||||
| 		Bind(id).ScanSingle(&task.ID, &task.Name, &task.Reward, &task.Assigned) | ||||
| 	err := db.UpdateScheduledTasks() | ||||
| 	if err != nil { | ||||
| 		return Task{}, err | ||||
| 		return Task{}, fmt.Errorf("failed to update scheduled tasks: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	var reward int64 | ||||
| 	err = db.db.Query("select id, name, reward, assigned, schedule from tasks where id = ? and completed is null"). | ||||
| 		Bind(id).ScanSingle(&task.ID, &task.Name, &reward, &task.Assigned, &task.Schedule) | ||||
| 	if err != nil { | ||||
| 		return task, err | ||||
| 	} | ||||
| 	task.Reward = float64(reward) / 100.0 | ||||
| 	return task, nil | ||||
| } | ||||
|  | ||||
| func (db *Db) UpdateScheduledTasks() error { | ||||
| 	type ScheduledTask struct { | ||||
| 		ID       int | ||||
| 		Schedule string | ||||
| 		Expires  int64 | ||||
| 	} | ||||
| 	tasks := make([]ScheduledTask, 0) | ||||
| 	var err error | ||||
|  | ||||
| 	for row := range db.db.Query("select id, schedule, next_run from tasks where schedule is not null").Range(&err) { | ||||
| 		task := ScheduledTask{} | ||||
| 		err := row.Scan(&task.ID, &task.Schedule, &task.Expires) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if time.Now().Unix() >= task.Expires { | ||||
| 			tasks = append(tasks, task) | ||||
| 		} | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to fetch scheduled tasks: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	tx, err := db.db.Begin() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer tx.MustRollback() | ||||
|  | ||||
| 	for _, task := range tasks { | ||||
| 		nextRun, err := gronx.NextTickAfter(task.Schedule, time.Now(), false) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("failed to calculate next run for task %d: %w", task.ID, err) | ||||
| 		} | ||||
|  | ||||
| 		err = tx.Query("insert into tasks (name, reward, assigned, schedule, next_run) select name, reward, assigned, schedule, ? from tasks where id = ?"). | ||||
| 			Bind(nextRun.Unix(), task.ID). | ||||
| 			Exec() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		err = tx.Query("update tasks set schedule = null where id = ?").Bind(task.ID).Exec() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		tx.Query("select last_insert_rowid()").MustScanSingle(&task.ID) | ||||
| 		log.Printf("Task %d scheduled for %s", task.ID, nextRun) | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return tx.Commit() | ||||
| } | ||||
|  | ||||
| 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 = ?"). | ||||
| @@ -227,8 +481,9 @@ func (db *Db) UpdateTask(id int, task *CreateTaskRequest) error { | ||||
| 	} | ||||
| 	defer tx.MustRollback() | ||||
|  | ||||
| 	reward := int(math.Round(task.Reward * 100.0)) | ||||
| 	err = tx.Query("update tasks set name=?, reward=?, assigned=? where id = ?"). | ||||
| 		Bind(task.Name, task.Reward, task.Assigned, id). | ||||
| 		Bind(task.Name, reward, task.Assigned, id). | ||||
| 		Exec() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| @@ -236,18 +491,291 @@ 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 (?, ?, ?)"). | ||||
| 		Bind(userId, time.Now().Unix(), allowance.Allowance). | ||||
| 	var reward int | ||||
| 	var rewardName string | ||||
| 	err = tx.Query("select reward, name from tasks where id = ?").Bind(taskId).ScanSingle(&reward, &rewardName) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	for userRow := range tx.Query("select id from users").Range(&err) { | ||||
| 		var userId int | ||||
| 		err = userRow.Scan(&userId) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		// Add the history entry | ||||
| 		err = tx.Query("insert into history (user_id, timestamp, amount, description) values (?, ?, ?, ?)"). | ||||
| 			Bind(userId, time.Now().Unix(), reward, fmt.Sprintf("Task completed: %s", rewardName)). | ||||
| 			Exec() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		err := db.addDistributedReward(tx, userId, reward) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Remove the task | ||||
| 	err = tx.Query("update tasks set completed=? where id = ?").Bind(time.Now().Unix(), taskId).Exec() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return tx.Commit() | ||||
| } | ||||
|  | ||||
| func (db *Db) addDistributedReward(tx *mysqlite.Tx, userId int, reward int) error { | ||||
| 	var userWeight float64 | ||||
| 	err := tx.Query("select weight from users where id = ?").Bind(userId).ScanSingle(&userWeight) | ||||
| 	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, target, balance from allowances where user_id = ? and weight > 0 order by (target - balance) asc").Bind(userId).Range(&err) { | ||||
| 			var allowanceId, allowanceTarget, allowanceBalance int | ||||
| 			var allowanceWeight float64 | ||||
| 			err = allowanceRow.Scan(&allowanceId, &allowanceWeight, &allowanceTarget, &allowanceBalance) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			// Calculate the amount to add to the allowance | ||||
| 			amount := int((allowanceWeight / sumOfWeights) * float64(remainingReward)) | ||||
| 			if allowanceBalance+amount > allowanceTarget { | ||||
| 				// If the amount reaches past the target, set it to the target | ||||
| 				amount = allowanceTarget - allowanceBalance | ||||
| 			} | ||||
| 			sumOfWeights -= allowanceWeight | ||||
| 			err = tx.Query("update allowances set balance = balance + ? where id = ? and user_id = ?"). | ||||
| 				Bind(amount, allowanceId, userId).Exec() | ||||
| 			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() | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| func (db *Db) AddHistory(userId int, allowance *PostHistory) error { | ||||
| 	tx, err := db.db.Begin() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer tx.MustRollback() | ||||
|  | ||||
| 	amount := int(math.Round(allowance.Allowance * 100.0)) | ||||
| 	err = tx.Query("insert into history (user_id, timestamp, amount, description) values (?, ?, ?, ?)"). | ||||
| 		Bind(userId, time.Now().Unix(), amount, allowance.Description). | ||||
| 		Exec() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	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`, description from history where user_id = ? order by `timestamp` desc"). | ||||
| 		Bind(userId).Range(&err) { | ||||
| 		allowance := History{} | ||||
| 		var timestamp, amount int64 | ||||
| 		err = row.Scan(&amount, ×tamp, &allowance.Description) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		allowance.Allowance = float64(amount) / 100.0 | ||||
| 		allowance.Timestamp = time.Unix(timestamp, 0) | ||||
| 		history = append(history, allowance) | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return history, nil | ||||
| } | ||||
|  | ||||
| func (db *Db) AddAllowanceAmount(userId int, allowanceId int, request AddAllowanceAmountRequest) error { | ||||
| 	tx, err := db.db.Begin() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer tx.MustRollback() | ||||
|  | ||||
| 	// Convert amount to integer (cents) | ||||
| 	remainingAmount := int(math.Round(request.Amount * 100)) | ||||
|  | ||||
| 	// Insert history entry | ||||
| 	err = tx.Query("insert into history (user_id, timestamp, amount, description) values (?, ?, ?, ?)"). | ||||
| 		Bind(userId, time.Now().Unix(), remainingAmount, request.Description). | ||||
| 		Exec() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if allowanceId == 0 { | ||||
| 		if remainingAmount < 0 { | ||||
| 			var userBalance int | ||||
| 			err = tx.Query("select balance from users where id = ?"). | ||||
| 				Bind(userId).ScanSingle(&userBalance) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			if remainingAmount > userBalance { | ||||
| 				return fmt.Errorf("cannot remove more than the current balance: %d", userBalance) | ||||
| 			} | ||||
| 		} | ||||
| 		err = tx.Query("update users set balance = balance + ? where id = ?"). | ||||
| 			Bind(remainingAmount, userId).Exec() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} else if remainingAmount < 0 { | ||||
| 		var progress int | ||||
| 		err = tx.Query("select balance from allowances where id = ? and user_id = ?"). | ||||
| 			Bind(allowanceId, userId).ScanSingle(&progress) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		if remainingAmount > progress { | ||||
| 			return fmt.Errorf("cannot remove more than the current allowance balance: %d", progress) | ||||
| 		} | ||||
|  | ||||
| 		err = tx.Query("update allowances set balance = balance + ? where id = ? and user_id = ?"). | ||||
| 			Bind(remainingAmount, allowanceId, userId).Exec() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} else { | ||||
| 		// Fetch the target and progress of the specified allowance | ||||
| 		var target, progress int | ||||
| 		err = tx.Query("select target, balance from allowances where id = ? and user_id = ?"). | ||||
| 			Bind(allowanceId, userId).ScanSingle(&target, &progress) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		// Calculate the amount to add to the current allowance | ||||
| 		toAdd := remainingAmount | ||||
| 		if progress+toAdd > target { | ||||
| 			toAdd = target - progress | ||||
| 		} | ||||
| 		remainingAmount -= toAdd | ||||
|  | ||||
| 		// Update the current allowance | ||||
| 		if toAdd > 0 { | ||||
| 			err = tx.Query("update allowances set balance = balance + ? where id = ? and user_id = ?"). | ||||
| 				Bind(toAdd, allowanceId, userId).Exec() | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// If there's remaining amount, distribute it to the user's allowances | ||||
| 		if remainingAmount > 0 { | ||||
| 			err = db.addDistributedReward(tx, userId, remainingAmount) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return tx.Commit() | ||||
| } | ||||
|  | ||||
| func (db *Db) TransferAllowance(fromId int, toId int, amount float64) error { | ||||
| 	if fromId == toId { | ||||
| 		return nil | ||||
| 	} | ||||
| 	amountCents := int(math.Round(amount * 100.0)) | ||||
| 	if amountCents <= 0 { | ||||
| 		return fmt.Errorf("amount must be positive") | ||||
| 	} | ||||
|  | ||||
| 	tx, err := db.db.Begin() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer tx.MustRollback() | ||||
|  | ||||
| 	// Fetch from allowance (user_id, balance) | ||||
| 	var fromUserId int | ||||
| 	var fromBalance int | ||||
| 	err = tx.Query("select user_id, balance from allowances where id = ?").Bind(fromId).ScanSingle(&fromUserId, &fromBalance) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Fetch to allowance (user_id, target, balance) | ||||
| 	var toUserId int | ||||
| 	var toTarget int | ||||
| 	var toBalance int | ||||
| 	err = tx.Query("select user_id, target, balance from allowances where id = ?").Bind(toId).ScanSingle(&toUserId, &toTarget, &toBalance) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Ensure same owner | ||||
| 	if fromUserId != toUserId { | ||||
| 		return fmt.Errorf(ErrDifferentUsers) | ||||
| 	} | ||||
|  | ||||
| 	// Calculate how much the 'to' goal still needs | ||||
| 	remainingTo := toTarget - toBalance | ||||
| 	if remainingTo <= 0 { | ||||
| 		// Nothing to transfer | ||||
| 		return fmt.Errorf("target already reached") | ||||
| 	} | ||||
|  | ||||
| 	// Limit transfer to what 'to' still needs | ||||
| 	transfer := amountCents | ||||
| 	if transfer > remainingTo { | ||||
| 		transfer = remainingTo | ||||
| 	} | ||||
|  | ||||
| 	// Ensure 'from' has enough balance | ||||
| 	if fromBalance < transfer { | ||||
| 		return fmt.Errorf(ErrInsufficientFunds) | ||||
| 	} | ||||
|  | ||||
| 	// Perform updates | ||||
| 	err = tx.Query("update allowances set balance = balance - ? where id = ? and user_id = ?").Bind(transfer, fromId, fromUserId).Exec() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	err = tx.Query("update allowances set balance = balance + ? where id = ? and user_id = ?").Bind(transfer, toId, toUserId).Exec() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return tx.Commit() | ||||
| } | ||||
|   | ||||
| @@ -1,45 +1,64 @@ | ||||
| package main | ||||
|  | ||||
| import "time" | ||||
|  | ||||
| type User struct { | ||||
| 	ID   int    `json:"id"` | ||||
| 	Name string `json:"name"` | ||||
| } | ||||
|  | ||||
| type UserWithAllowance struct { | ||||
| 	ID        int    `json:"id"` | ||||
| 	Name      string `json:"name"` | ||||
| 	Allowance int    `json:"allowance"` | ||||
| 	ID        int     `json:"id"` | ||||
| 	Name      string  `json:"name"` | ||||
| 	Allowance float64 `json:"allowance"` | ||||
| } | ||||
|  | ||||
| type Allowance struct { | ||||
| 	Allowance int    `json:"allowance"` | ||||
| 	Goals     []Goal `json:"goals"` | ||||
| type History struct { | ||||
| 	Allowance   float64   `json:"allowance"` | ||||
| 	Timestamp   time.Time `json:"timestamp"` | ||||
| 	Description string    `json:"description"` | ||||
| } | ||||
|  | ||||
| type PostAllowance struct { | ||||
| 	Allowance int `json:"allowance"` | ||||
| type PostHistory struct { | ||||
| 	Allowance   float64 `json:"allowance"` | ||||
| 	Description string  `json:"description"` | ||||
| } | ||||
|  | ||||
| // Task represents a task in the system. | ||||
| type Task struct { | ||||
| 	ID       int    `json:"id"` | ||||
| 	Name     string `json:"name"` | ||||
| 	Reward   int    `json:"reward"` | ||||
| 	Assigned *int   `json:"assigned"` // Pointer to allow null | ||||
| 	ID       int     `json:"id"` | ||||
| 	Name     string  `json:"name"` | ||||
| 	Reward   float64 `json:"reward"` | ||||
| 	Assigned *int    `json:"assigned"` | ||||
| 	Schedule *string `json:"schedule"` | ||||
| } | ||||
|  | ||||
| type Goal struct { | ||||
| 	ID       int    `json:"id"` | ||||
| 	Name     string `json:"name"` | ||||
| 	Target   int    `json:"target"` | ||||
| 	Progress int    `json:"progress"` | ||||
| 	Weight   int    `json:"weight"` | ||||
| type Allowance struct { | ||||
| 	ID       int     `json:"id"` | ||||
| 	Name     string  `json:"name"` | ||||
| 	Target   float64 `json:"target"` | ||||
| 	Progress float64 `json:"progress"` | ||||
| 	Weight   float64 `json:"weight"` | ||||
| 	Colour   string  `json:"colour"` | ||||
| } | ||||
|  | ||||
| type CreateGoalRequest struct { | ||||
| 	Name   string `json:"name"` | ||||
| 	Target int    `json:"target"` | ||||
| 	Weight int    `json:"weight"` | ||||
| type CreateAllowanceRequest struct { | ||||
| 	Name   string  `json:"name"` | ||||
| 	Target float64 `json:"target"` | ||||
| 	Weight float64 `json:"weight"` | ||||
| 	Colour string  `json:"colour"` | ||||
| } | ||||
|  | ||||
| type UpdateAllowanceRequest struct { | ||||
| 	Name   string  `json:"name"` | ||||
| 	Target float64 `json:"target"` | ||||
| 	Weight float64 `json:"weight"` | ||||
| 	Colour string  `json:"colour"` | ||||
| } | ||||
|  | ||||
| type BulkUpdateAllowanceRequest struct { | ||||
| 	ID     int     `json:"id"` | ||||
| 	Weight float64 `json:"weight"` | ||||
| } | ||||
|  | ||||
| type CreateGoalResponse struct { | ||||
| @@ -47,11 +66,23 @@ type CreateGoalResponse struct { | ||||
| } | ||||
|  | ||||
| type CreateTaskRequest struct { | ||||
| 	Name     string `json:"name" binding:"required"` | ||||
| 	Reward   int    `json:"reward"` | ||||
| 	Assigned *int   `json:"assigned"` | ||||
| 	Name     string  `json:"name" binding:"required"` | ||||
| 	Reward   float64 `json:"reward"` | ||||
| 	Assigned *int    `json:"assigned"` | ||||
| 	Schedule *string `json:"schedule"` | ||||
| } | ||||
|  | ||||
| type CreateTaskResponse struct { | ||||
| 	ID int `json:"id"` | ||||
| } | ||||
|  | ||||
| type AddAllowanceAmountRequest struct { | ||||
| 	Amount      float64 `json:"amount"` | ||||
| 	Description string  `json:"description"` | ||||
| } | ||||
|  | ||||
| type TransferRequest struct { | ||||
| 	From   int     `json:"from"` | ||||
| 	To     int     `json:"to"` | ||||
| 	Amount float64 `json:"amount"` | ||||
| } | ||||
|   | ||||
| @@ -3,29 +3,34 @@ module allowance_planner | ||||
| go 1.24.2 | ||||
|  | ||||
| require ( | ||||
| 	gitea.seeseepuff.be/seeseemelk/mysqlite v0.11.1 | ||||
| 	gitea.seeseepuff.be/seeseemelk/mysqlite v0.15.0 | ||||
| 	github.com/adhocore/gronx v1.19.6 | ||||
| 	github.com/gavv/httpexpect/v2 v2.17.0 | ||||
| 	github.com/gin-gonic/gin v1.10.0 | ||||
| 	github.com/gin-contrib/cors v1.7.6 | ||||
| 	github.com/gin-gonic/gin v1.11.0 | ||||
| 	github.com/stretchr/testify v1.11.1 | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| 	github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect | ||||
| 	github.com/ajg/form v1.5.1 // indirect | ||||
| 	github.com/andybalholm/brotli v1.1.1 // indirect | ||||
| 	github.com/bytedance/sonic v1.13.2 // indirect | ||||
| 	github.com/bytedance/sonic/loader v0.2.4 // indirect | ||||
| 	github.com/cloudwego/base64x v0.1.5 // indirect | ||||
| 	github.com/andybalholm/brotli v1.2.0 // indirect | ||||
| 	github.com/bytedance/gopkg v0.1.3 // indirect | ||||
| 	github.com/bytedance/sonic v1.14.1 // indirect | ||||
| 	github.com/bytedance/sonic/loader v0.3.0 // indirect | ||||
| 	github.com/cloudwego/base64x v0.1.6 // indirect | ||||
| 	github.com/davecgh/go-spew v1.1.1 // indirect | ||||
| 	github.com/dustin/go-humanize v1.0.1 // indirect | ||||
| 	github.com/fatih/color v1.18.0 // indirect | ||||
| 	github.com/fatih/structs v1.1.0 // indirect | ||||
| 	github.com/gabriel-vasile/mimetype v1.4.9 // indirect | ||||
| 	github.com/gabriel-vasile/mimetype v1.4.10 // indirect | ||||
| 	github.com/gin-contrib/sse v1.1.0 // indirect | ||||
| 	github.com/go-playground/locales v0.14.1 // indirect | ||||
| 	github.com/go-playground/universal-translator v0.18.1 // indirect | ||||
| 	github.com/go-playground/validator/v10 v10.26.0 // indirect | ||||
| 	github.com/go-playground/validator/v10 v10.28.0 // indirect | ||||
| 	github.com/gobwas/glob v0.2.3 // indirect | ||||
| 	github.com/goccy/go-json v0.10.5 // indirect | ||||
| 	github.com/goccy/go-yaml v1.18.0 // indirect | ||||
| 	github.com/google/go-querystring v1.1.0 // indirect | ||||
| 	github.com/google/uuid v1.6.0 // indirect | ||||
| 	github.com/gorilla/websocket v1.5.3 // indirect | ||||
| @@ -33,44 +38,49 @@ require ( | ||||
| 	github.com/imkira/go-interpol v1.1.0 // indirect | ||||
| 	github.com/json-iterator/go v1.1.12 // indirect | ||||
| 	github.com/klauspost/compress v1.18.0 // indirect | ||||
| 	github.com/klauspost/cpuid/v2 v2.2.10 // indirect | ||||
| 	github.com/klauspost/cpuid/v2 v2.3.0 // indirect | ||||
| 	github.com/leodido/go-urn v1.4.0 // indirect | ||||
| 	github.com/mattn/go-colorable v0.1.14 // indirect | ||||
| 	github.com/mattn/go-isatty v0.0.20 // indirect | ||||
| 	github.com/mitchellh/go-wordwrap v1.0.1 // indirect | ||||
| 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | ||||
| 	github.com/modern-go/reflect2 v1.0.2 // indirect | ||||
| 	github.com/ncruces/go-strftime v0.1.9 // indirect | ||||
| 	github.com/ncruces/go-strftime v1.0.0 // indirect | ||||
| 	github.com/pelletier/go-toml/v2 v2.2.4 // indirect | ||||
| 	github.com/pmezard/go-difflib v1.0.0 // indirect | ||||
| 	github.com/quic-go/qpack v0.5.1 // indirect | ||||
| 	github.com/quic-go/quic-go v0.55.0 // indirect | ||||
| 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect | ||||
| 	github.com/sanity-io/litter v1.5.8 // indirect | ||||
| 	github.com/sergi/go-diff v1.3.1 // indirect | ||||
| 	github.com/stretchr/testify v1.10.0 // indirect | ||||
| 	github.com/sergi/go-diff v1.4.0 // indirect | ||||
| 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | ||||
| 	github.com/ugorji/go/codec v1.2.12 // indirect | ||||
| 	github.com/ugorji/go/codec v1.3.0 // indirect | ||||
| 	github.com/valyala/bytebufferpool v1.0.0 // indirect | ||||
| 	github.com/valyala/fasthttp v1.62.0 // indirect | ||||
| 	github.com/valyala/fasthttp v1.67.0 // indirect | ||||
| 	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect | ||||
| 	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect | ||||
| 	github.com/xeipuuv/gojsonschema v1.2.0 // indirect | ||||
| 	github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect | ||||
| 	github.com/yudai/gojsondiff v1.0.0 // indirect | ||||
| 	github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect | ||||
| 	golang.org/x/arch v0.17.0 // indirect | ||||
| 	golang.org/x/crypto v0.38.0 // indirect | ||||
| 	golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect | ||||
| 	golang.org/x/net v0.40.0 // indirect | ||||
| 	golang.org/x/sys v0.33.0 // indirect | ||||
| 	golang.org/x/text v0.25.0 // indirect | ||||
| 	google.golang.org/protobuf v1.36.6 // indirect | ||||
| 	go.uber.org/mock v0.6.0 // indirect | ||||
| 	golang.org/x/arch v0.22.0 // indirect | ||||
| 	golang.org/x/crypto v0.42.0 // indirect | ||||
| 	golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect | ||||
| 	golang.org/x/mod v0.29.0 // indirect | ||||
| 	golang.org/x/net v0.45.0 // indirect | ||||
| 	golang.org/x/sync v0.17.0 // indirect | ||||
| 	golang.org/x/sys v0.37.0 // indirect | ||||
| 	golang.org/x/text v0.30.0 // indirect | ||||
| 	golang.org/x/tools v0.37.0 // indirect | ||||
| 	google.golang.org/protobuf v1.36.10 // indirect | ||||
| 	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.66.10 // indirect | ||||
| 	modernc.org/mathutil v1.7.1 // indirect | ||||
| 	modernc.org/memory v1.10.0 // indirect | ||||
| 	modernc.org/sqlite v1.37.0 // indirect | ||||
| 	modernc.org/memory v1.11.0 // indirect | ||||
| 	modernc.org/sqlite v1.39.0 // indirect | ||||
| 	moul.io/http2curl/v2 v2.3.0 // indirect | ||||
| 	zombiezen.com/go/sqlite v1.4.0 // indirect | ||||
| 	zombiezen.com/go/sqlite v1.4.2 // indirect | ||||
| ) | ||||
|   | ||||
							
								
								
									
										158
									
								
								backend/go.sum
									
									
									
									
									
								
							
							
						
						| @@ -1,19 +1,21 @@ | ||||
| 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.15.0 h1:+k0iBYM/aZJxz7++EKi/G9e66E9u4bPS3DFLrBeDb9Y= | ||||
| gitea.seeseepuff.be/seeseemelk/mysqlite v0.15.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/adhocore/gronx v1.19.6 h1:5KNVcoR9ACgL9HhEqCm5QXsab/gI4QDIybTAWcXDKDc= | ||||
| github.com/adhocore/gronx v1.19.6/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg= | ||||
| github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= | ||||
| github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= | ||||
| github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= | ||||
| github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= | ||||
| github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= | ||||
| github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= | ||||
| github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= | ||||
| github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= | ||||
| github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= | ||||
| github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= | ||||
| github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= | ||||
| github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= | ||||
| github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= | ||||
| github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= | ||||
| github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= | ||||
| github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= | ||||
| github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= | ||||
| github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= | ||||
| github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= | ||||
| github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= | ||||
| github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= | ||||
| github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= | ||||
| github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| @@ -24,29 +26,33 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= | ||||
| github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= | ||||
| github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= | ||||
| github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= | ||||
| github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= | ||||
| github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= | ||||
| github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= | ||||
| github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= | ||||
| 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.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= | ||||
| github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= | ||||
| 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= | ||||
| github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= | ||||
| github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= | ||||
| github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= | ||||
| github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= | ||||
| github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= | ||||
| github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= | ||||
| github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= | ||||
| github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= | ||||
| github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | ||||
| github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= | ||||
| github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= | ||||
| github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= | ||||
| github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= | ||||
| github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= | ||||
| github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= | ||||
| github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= | ||||
| github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= | ||||
| github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= | ||||
| github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= | ||||
| github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= | ||||
| github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | ||||
| github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= | ||||
| github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= | ||||
| github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= | ||||
| github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= | ||||
| github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||
| @@ -64,12 +70,11 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr | ||||
| github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= | ||||
| github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= | ||||
| github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= | ||||
| github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | ||||
| 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/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= | ||||
| github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= | ||||
| 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= | ||||
| @@ -86,8 +91,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w | ||||
| github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||
| github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= | ||||
| github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= | ||||
| github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= | ||||
| github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= | ||||
| github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= | ||||
| github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= | ||||
| github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= | ||||
| github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= | ||||
| github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= | ||||
| @@ -98,34 +103,39 @@ github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnI | ||||
| github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= | ||||
| github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= | ||||
| github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= | ||||
| github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= | ||||
| 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= | ||||
| github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= | ||||
| github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= | ||||
| github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= | ||||
| github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | ||||
| github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | ||||
| github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | ||||
| github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||
| github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | ||||
| github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||||
| github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||
| github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= | ||||
| github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||||
| github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= | ||||
| github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= | ||||
| github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= | ||||
| github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= | ||||
| github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= | ||||
| github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= | ||||
| github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= | ||||
| github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= | ||||
| github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= | ||||
| github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= | ||||
| github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= | ||||
| github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0= | ||||
| github.com/valyala/fasthttp v1.62.0/go.mod h1:FCINgr4GKdKqV8Q0xv8b+UxPV+H/O5nNFo3D+r54Htg= | ||||
| github.com/valyala/fasthttp v1.67.0 h1:tqKlJMUP6iuNG8hGjK/s9J4kadH7HLV4ijEcPGsezac= | ||||
| github.com/valyala/fasthttp v1.67.0/go.mod h1:qYSIpqt/0XNmShgo/8Aq8E3UYWVVwNS2QYmzd8WIEPM= | ||||
| github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= | ||||
| github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= | ||||
| github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= | ||||
| @@ -144,52 +154,55 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDf | ||||
| github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI= | ||||
| github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= | ||||
| github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | ||||
| golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= | ||||
| golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= | ||||
| go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= | ||||
| go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= | ||||
| golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= | ||||
| golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= | ||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
| golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||
| golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= | ||||
| golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= | ||||
| golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= | ||||
| golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= | ||||
| golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= | ||||
| golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= | ||||
| golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= | ||||
| golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= | ||||
| golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= | ||||
| golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= | ||||
| golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= | ||||
| golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= | ||||
| golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||
| golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= | ||||
| golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= | ||||
| golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= | ||||
| golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= | ||||
| golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= | ||||
| golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= | ||||
| golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= | ||||
| golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= | ||||
| golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= | ||||
| golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= | ||||
| golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= | ||||
| golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= | ||||
| golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= | ||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= | ||||
| golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= | ||||
| golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= | ||||
| golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= | ||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||
| golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= | ||||
| golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= | ||||
| golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= | ||||
| golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= | ||||
| golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= | ||||
| golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= | ||||
| google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= | ||||
| google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= | ||||
| google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= | ||||
| 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= | ||||
| @@ -200,32 +213,33 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| 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/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8= | ||||
| modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= | ||||
| modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= | ||||
| modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= | ||||
| modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= | ||||
| modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= | ||||
| modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= | ||||
| modernc.org/fileutil v1.3.40/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/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= | ||||
| modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= | ||||
| modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= | ||||
| modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= | ||||
| 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= | ||||
| modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= | ||||
| modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= | ||||
| modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= | ||||
| modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY= | ||||
| modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= | ||||
| modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= | ||||
| modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= | ||||
| modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= | ||||
| modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= | ||||
| moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= | ||||
| moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= | ||||
| nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= | ||||
| zombiezen.com/go/sqlite v1.4.0 h1:N1s3RIljwtp4541Y8rM880qgGIgq3fTD2yks1xftnKU= | ||||
| zombiezen.com/go/sqlite v1.4.0/go.mod h1:0w9F1DN9IZj9AcLS9YDKMboubCACkwYCGkzoy3eG5ik= | ||||
| zombiezen.com/go/sqlite v1.4.2 h1:KZXLrBuJ7tKNEm+VJcApLMeQbhmAUOKA5VWS93DfFRo= | ||||
| zombiezen.com/go/sqlite v1.4.2/go.mod h1:5Kd4taTAD4MkBzT25mQ9uaAlLjyR0rFhsR6iINO70jc= | ||||
|   | ||||
							
								
								
									
										448
									
								
								backend/main.go
									
									
									
									
									
								
							
							
						
						| @@ -4,13 +4,15 @@ import ( | ||||
| 	"context" | ||||
| 	"embed" | ||||
| 	"errors" | ||||
| 	"gitea.seeseepuff.be/seeseemelk/mysqlite" | ||||
| 	"log" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"strconv" | ||||
|  | ||||
| 	"gitea.seeseepuff.be/seeseemelk/mysqlite" | ||||
|  | ||||
| 	"github.com/gin-contrib/cors" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| ) | ||||
|  | ||||
| @@ -23,6 +25,8 @@ const ( | ||||
| 	ErrInvalidUserID       = "Invalid user ID" | ||||
| 	ErrUserNotFound        = "User not found" | ||||
| 	ErrCheckingUserExist   = "Error checking user existence: %v" | ||||
| 	ErrInsufficientFunds   = "Insufficient funds in source allowance" | ||||
| 	ErrDifferentUsers      = "Allowances do not belong to the same user" | ||||
| ) | ||||
|  | ||||
| // ServerConfig holds configuration for the server. | ||||
| @@ -42,6 +46,11 @@ type ServerConfig struct { | ||||
| 	Started chan bool | ||||
| } | ||||
|  | ||||
| const DefaultDomain = "localhost:8080" | ||||
|  | ||||
| // The domain that the server is reachable at. | ||||
| var domain = DefaultDomain | ||||
|  | ||||
| func getUsers(c *gin.Context) { | ||||
| 	users, err := db.GetUsers() | ||||
| 	if err != nil { | ||||
| @@ -75,7 +84,7 @@ func getUser(c *gin.Context) { | ||||
| 	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 +105,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 +167,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 +176,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 +197,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 +207,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 +218,212 @@ 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 addToAllowance(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 AddAllowanceAmountRequest | ||||
| 	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.AddAllowanceAmount(userId, allowanceId, allowanceRequest) | ||||
| 	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) { | ||||
| @@ -201,6 +439,11 @@ func createTask(c *gin.Context) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if taskRequest.Schedule != nil { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "Schedules are not yet supported"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// If assigned is not nil, check if user exists | ||||
| 	if taskRequest.Assigned != nil { | ||||
| 		exists, err := db.UserExists(*taskRequest.Assigned) | ||||
| @@ -233,7 +476,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) { | ||||
| @@ -278,6 +521,11 @@ func putTask(c *gin.Context) { | ||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"}) | ||||
| 		return | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		log.Printf("Error getting task: %v", err) | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err = db.UpdateTask(taskId, &taskRequest) | ||||
| 	if err != nil { | ||||
| @@ -289,7 +537,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,13 +600,18 @@ 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 | ||||
| 	} | ||||
|  | ||||
| 	if historyRequest.Description == "" { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "Description cannot be empty"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	exists, err := db.UserExists(userId) | ||||
| 	if err != nil { | ||||
| 		log.Printf(ErrCheckingUserExist, err) | ||||
| @@ -316,17 +623,61 @@ func postAllowance(c *gin.Context) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err = db.AddAllowance(userId, &allowanceRequest) | ||||
| 	err = db.AddHistory(userId, &historyRequest) | ||||
| 	if err != nil { | ||||
| 		log.Printf("Error updating allowance: %v", err) | ||||
| 		log.Printf("Error updating history: %v", err) | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) | ||||
| 		return | ||||
| 	} | ||||
| 	c.JSON(http.StatusOK, gin.H{"message": "Allowance updated successfully"}) | ||||
| 	c.JSON(http.StatusOK, gin.H{"message": "History updated successfully"}) | ||||
| } | ||||
|  | ||||
| func getHistory(c *gin.Context) { | ||||
| 	userIdStr := c.Param("userId") | ||||
| 	userId, err := strconv.Atoi(userIdStr) | ||||
| 	if err != nil { | ||||
| 		log.Printf("Invalid user ID: %v", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	history, err := db.GetHistory(userId) | ||||
| 	if err != nil { | ||||
| 		log.Printf("Error getting history: %v", err) | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	c.IndentedJSON(http.StatusOK, history) | ||||
| } | ||||
|  | ||||
| func transfer(c *gin.Context) { | ||||
| 	var transferRequest TransferRequest | ||||
| 	if err := c.ShouldBindJSON(&transferRequest); err != nil { | ||||
| 		log.Printf("Error parsing request body: %v", err) | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err := db.TransferAllowance(transferRequest.From, transferRequest.To, transferRequest.Amount) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, mysqlite.ErrNoRows) { | ||||
| 			c.JSON(http.StatusNotFound, gin.H{"error": "Allowance not found"}) | ||||
| 			return | ||||
| 		} | ||||
| 		if err.Error() == ErrInsufficientFunds || err.Error() == ErrDifferentUsers { | ||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 			return | ||||
| 		} | ||||
| 		log.Printf("Error transferring allowance: %v", err) | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	c.JSON(http.StatusOK, gin.H{"message": "Transfer successful"}) | ||||
| } | ||||
|  | ||||
| /* | ||||
| * | ||||
| Initialises the database, and then starts the server. | ||||
| If the context gets cancelled, the server is shutdown and the database is closed. | ||||
| */ | ||||
| @@ -335,16 +686,33 @@ func start(ctx context.Context, config *ServerConfig) { | ||||
| 	defer db.db.MustClose() | ||||
|  | ||||
| 	router := gin.Default() | ||||
|  | ||||
| 	corsConfig := cors.DefaultConfig() | ||||
| 	corsConfig.AllowAllOrigins = true | ||||
| 	router.Use(cors.New(corsConfig)) | ||||
|  | ||||
| 	// Web endpoints | ||||
| 	loadWebEndpoints(router) | ||||
| 	// API endpoints | ||||
| 	router.GET("/api/users", getUsers) | ||||
| 	router.GET("/api/user/:userId", getUser) | ||||
| 	router.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/user/:userId/allowance/:allowanceId/add", addToAllowance) | ||||
| 	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) | ||||
| 	router.POST("/api/transfer", transfer) | ||||
|  | ||||
| 	srv := &http.Server{ | ||||
| 		Addr:    config.Addr, | ||||
| @@ -377,6 +745,16 @@ 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) | ||||
| 	} | ||||
| 	domain = os.Getenv("DOMAIN") | ||||
| 	if domain == "" { | ||||
| 		domain = DefaultDomain | ||||
| 		log.Printf("Warning: No DOMAIN set, using default of %s", domain) | ||||
| 	} | ||||
| 	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 10.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 | ||||
| ); | ||||
|  | ||||
|   | ||||
							
								
								
									
										2
									
								
								backend/migrations/2_add_colour.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| alter table allowances | ||||
| add column colour integer; | ||||
							
								
								
									
										1
									
								
								backend/migrations/3_change_weight_default.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| update users set weight = 10.0 where weight = 0.0; | ||||
							
								
								
									
										2
									
								
								backend/migrations/4_add_history_description.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| alter table history | ||||
| add column description text; | ||||
							
								
								
									
										3
									
								
								backend/migrations/5_add_schedules.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| alter table tasks add column schedule text; | ||||
| alter table tasks add column completed date; | ||||
| alter table tasks add column next_run date; | ||||
							
								
								
									
										249
									
								
								backend/web.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,249 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| ) | ||||
|  | ||||
| type ViewModel struct { | ||||
| 	Users       []User | ||||
| 	CurrentUser int | ||||
| 	Allowances  []Allowance | ||||
| 	Tasks       []Task | ||||
| 	History     []History | ||||
| 	Error       string | ||||
| } | ||||
|  | ||||
| func loadWebEndpoints(router *gin.Engine) { | ||||
| 	router.LoadHTMLFiles("web.gohtml") | ||||
| 	router.GET("/", renderIndex) | ||||
| 	router.GET("/login", renderLogin) | ||||
| 	router.POST("/createTask", renderCreateTask) | ||||
| 	router.GET("/completeTask", renderCompleteTask) | ||||
| 	router.POST("/createAllowance", renderCreateAllowance) | ||||
| 	router.GET("/completeAllowance", renderCompleteAllowance) | ||||
| } | ||||
|  | ||||
| func redirectToPage(c *gin.Context, page string) { | ||||
| 	redirectToPageStatus(c, page, http.StatusSeeOther) | ||||
| } | ||||
|  | ||||
| func redirectToPageStatus(c *gin.Context, page string, status int) { | ||||
| 	scheme := c.Request.URL.Scheme | ||||
| 	target := scheme + domain + page | ||||
| 	c.Redirect(status, target) | ||||
| } | ||||
|  | ||||
| func renderLogin(c *gin.Context) { | ||||
| 	if c.Query("user") != "" { | ||||
| 		log.Println("Set cookie for user:", c.Query("user")) | ||||
| 		c.SetCookie("user", c.Query("user"), 3600, "", "", false, true) | ||||
| 	} | ||||
| 	redirectToPage(c, "/") | ||||
| } | ||||
|  | ||||
| func renderIndex(c *gin.Context) { | ||||
| 	currentUser := getCurrentUser(c) | ||||
| 	if currentUser == nil { | ||||
| 		return | ||||
| 	} | ||||
| 	renderWithUser(c, *currentUser) | ||||
| } | ||||
|  | ||||
| func renderCreateTask(c *gin.Context) { | ||||
| 	currentUser := getCurrentUser(c) | ||||
| 	if currentUser == nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	name := c.PostForm("name") | ||||
| 	rewardStr := c.PostForm("reward") | ||||
| 	reward, err := strconv.ParseFloat(rewardStr, 64) | ||||
| 	if err != nil { | ||||
| 		renderError(c, http.StatusBadRequest, err) | ||||
| 		return | ||||
| 	} | ||||
| 	if name == "" || reward <= 0 { | ||||
| 		renderError(c, http.StatusBadRequest, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	request := &CreateTaskRequest{ | ||||
| 		Name:   name, | ||||
| 		Reward: reward, | ||||
| 	} | ||||
|  | ||||
| 	schedule := c.PostForm("schedule") | ||||
| 	if schedule != "" { | ||||
| 		request.Schedule = &schedule | ||||
| 	} | ||||
| 	 | ||||
| 	_, err = db.CreateTask(request) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		renderError(c, http.StatusInternalServerError, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	redirectToPageStatus(c, "/", http.StatusFound) | ||||
| } | ||||
|  | ||||
| func renderCompleteTask(c *gin.Context) { | ||||
| 	taskIDStr := c.Query("task") | ||||
| 	taskID, err := strconv.Atoi(taskIDStr) | ||||
| 	if err != nil { | ||||
| 		renderError(c, http.StatusBadRequest, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err = db.CompleteTask(taskID) | ||||
| 	if err != nil { | ||||
| 		renderError(c, http.StatusInternalServerError, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	redirectToPageStatus(c, "/", http.StatusFound) | ||||
| } | ||||
|  | ||||
| func renderCreateAllowance(c *gin.Context) { | ||||
| 	currentUser := getCurrentUser(c) | ||||
| 	if currentUser == nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	name := c.PostForm("name") | ||||
| 	targetStr := c.PostForm("target") | ||||
| 	target, err := strconv.ParseFloat(targetStr, 64) | ||||
| 	if err != nil { | ||||
| 		renderError(c, http.StatusBadRequest, err) | ||||
| 		return | ||||
| 	} | ||||
| 	weightStr := c.PostForm("weight") | ||||
| 	weight, err := strconv.ParseFloat(weightStr, 64) | ||||
| 	if err != nil { | ||||
| 		renderError(c, http.StatusBadRequest, err) | ||||
| 		return | ||||
| 	} | ||||
| 	if name == "" || target <= 0 || weight <= 0 { | ||||
| 		renderError(c, http.StatusBadRequest, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	_, err = db.CreateAllowance(*currentUser, &CreateAllowanceRequest{ | ||||
| 		Name:   name, | ||||
| 		Target: target, | ||||
| 		Weight: weight, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		renderError(c, http.StatusInternalServerError, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	redirectToPageStatus(c, "/", http.StatusFound) | ||||
| } | ||||
|  | ||||
| func renderCompleteAllowance(c *gin.Context) { | ||||
| 	currentUser := getCurrentUser(c) | ||||
| 	if currentUser == nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	allowanceIDStr := c.Query("allowance") | ||||
| 	allowanceID, err := strconv.Atoi(allowanceIDStr) | ||||
| 	if err != nil { | ||||
| 		renderError(c, http.StatusBadRequest, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err = db.CompleteAllowance(*currentUser, allowanceID) | ||||
| 	if err != nil { | ||||
| 		renderError(c, http.StatusInternalServerError, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	redirectToPageStatus(c, "/", http.StatusFound) | ||||
| } | ||||
|  | ||||
| func getCurrentUser(c *gin.Context) *int { | ||||
| 	currentUserStr, err := c.Cookie("user") | ||||
| 	log.Println("Cookie string:", currentUserStr) | ||||
| 	if errors.Is(err, http.ErrNoCookie) { | ||||
| 		renderNoUser(c) | ||||
| 		return nil | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		unsetUserCookie(c) | ||||
| 		return nil | ||||
| 	} | ||||
| 	currentUser, err := strconv.Atoi(currentUserStr) | ||||
| 	if err != nil { | ||||
| 		unsetUserCookie(c) | ||||
| 		return nil | ||||
| 	} | ||||
| 	userExists, err := db.UserExists(currentUser) | ||||
| 	if !userExists || err != nil { | ||||
| 		unsetUserCookie(c) | ||||
| 		return nil | ||||
| 	} | ||||
| 	return ¤tUser | ||||
| } | ||||
|  | ||||
| func unsetUserCookie(c *gin.Context) { | ||||
| 	c.SetCookie("user", "", -1, "/", "localhost", false, true) | ||||
| 	redirectToPageStatus(c, "/", http.StatusFound) | ||||
| } | ||||
|  | ||||
| func renderNoUser(c *gin.Context) { | ||||
| 	users, err := db.GetUsers() | ||||
| 	if err != nil { | ||||
| 		renderError(c, http.StatusInternalServerError, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	c.HTML(http.StatusOK, "web.gohtml", ViewModel{ | ||||
| 		Users: users, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func renderWithUser(c *gin.Context, currentUser int) { | ||||
| 	users, err := db.GetUsers() | ||||
| 	if err != nil { | ||||
| 		renderError(c, http.StatusInternalServerError, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	allowances, err := db.GetUserAllowances(currentUser) | ||||
| 	if err != nil { | ||||
| 		renderError(c, http.StatusInternalServerError, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	tasks, err := db.GetTasks() | ||||
| 	if err != nil { | ||||
| 		renderError(c, http.StatusInternalServerError, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	history, err := db.GetHistory(currentUser) | ||||
| 	if err != nil { | ||||
| 		renderError(c, http.StatusInternalServerError, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	c.HTML(http.StatusOK, "web.gohtml", ViewModel{ | ||||
| 		Users:       users, | ||||
| 		CurrentUser: currentUser, | ||||
| 		Allowances:  allowances, | ||||
| 		Tasks:       tasks, | ||||
| 		History:     history, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func renderError(c *gin.Context, statusCode int, err error) { | ||||
| 	c.HTML(statusCode, "web.gohtml", ViewModel{ | ||||
| 		Error: err.Error(), | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										137
									
								
								backend/web.gohtml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,137 @@ | ||||
| {{- /*gotype: allowance_planner.ViewModel*/}} | ||||
| <html lang="en"> | ||||
| <head> | ||||
| 	<title>Allowance Planner 2000</title> | ||||
| 	<style> | ||||
| 		<!-- | ||||
| 		tr:hover { | ||||
| 			background-color: #f0f0f0; | ||||
| 		} | ||||
| 		--> | ||||
| 	</style> | ||||
| </head> | ||||
| <body> | ||||
| <h1>Allowance Planner 2000</h1> | ||||
|  | ||||
| {{if ne .Error ""}} | ||||
| 	<h2>Error</h2> | ||||
| 	<p>{{.Error}}</p> | ||||
| {{else}} | ||||
| 	<h2>Users</h2> | ||||
| 	{{range .Users}} | ||||
| 		{{if eq $.CurrentUser .ID}} | ||||
| 			<strong>{{.Name}}</strong> | ||||
| 		{{else}} | ||||
| 			<a href="/login?user={{.ID}}">{{.Name}}</a> | ||||
| 		{{end}} | ||||
| 	{{end}} | ||||
|  | ||||
| 	{{if ne .CurrentUser 0}} | ||||
| 		<h2>Allowances</h2> | ||||
| 		<form action="/createAllowance" method="post"> | ||||
| 			<table border=1> | ||||
| 				<thead> | ||||
| 				<tr> | ||||
| 					<th>Name</th> | ||||
| 					<th>Progress</th> | ||||
| 					<th>Target</th> | ||||
| 					<th>Weight</th> | ||||
| 					<th>Actions</th> | ||||
| 				</tr> | ||||
| 				</thead> | ||||
| 				<tbody> | ||||
| 					<tr> | ||||
| 						<td><label><input type="text" name="name" placeholder="Name"></label></td> | ||||
| 						<td></td> | ||||
| 						<td><label><input type="number" name="target" placeholder="Target"></label></td> | ||||
| 						<td><label><input type="number" name="weight" placeholder="Weight"></label></td> | ||||
| 						<td><input type="submit" value="Create"></td> | ||||
| 					</tr> | ||||
| 				{{range .Allowances}} | ||||
| 					{{if eq .ID 0}} | ||||
| 						<tr> | ||||
| 							<td>Total</td> | ||||
| 							<td>{{.Progress}}</td> | ||||
| 							<td></td> | ||||
| 							<td>{{.Weight}}</td> | ||||
| 						</tr> | ||||
| 					{{else}} | ||||
| 						<tr> | ||||
| 							<td>{{.Name}}</td> | ||||
| 							<td><progress max="{{.Target}}" value="{{.Progress}}"></progress> ({{.Progress}})</td> | ||||
| 							<td>{{.Target}}</td> | ||||
| 							<td>{{.Weight}}</td> | ||||
|                             {{if ge .Progress .Target}} | ||||
| 								<td> | ||||
| 									<a href="/completeAllowance?allowance={{.ID}}">Mark as completed</a> | ||||
| 								</td> | ||||
|                             {{end}} | ||||
| 						</tr> | ||||
| 					{{end}} | ||||
| 				{{end}} | ||||
| 				</tbody> | ||||
| 			</table> | ||||
| 		</form> | ||||
|  | ||||
| 		<h2>Tasks</h2> | ||||
| 		<form method="post" action="/createTask"> | ||||
| 			<table border="1"> | ||||
| 				<thead> | ||||
| 				<tr> | ||||
| 					<th>Name</th> | ||||
| 					<th>Assigned</th> | ||||
| 					<th>Reward</th> | ||||
| 					<th>Schedule</th> | ||||
| 					<th>Actions</th> | ||||
| 				</tr> | ||||
| 				</thead> | ||||
| 				<tbody> | ||||
| 				{{range .Tasks}} | ||||
| 					<tr> | ||||
| 						<td>{{.Name}}</td> | ||||
| 						<td> | ||||
| 							{{if eq .Assigned nil}} | ||||
| 								None | ||||
| 							{{else}} | ||||
| 								{{.Assigned}} | ||||
| 							{{end}} | ||||
| 						</td> | ||||
| 						<td>{{.Reward}}</td> | ||||
| 						<td>{{.Schedule}}</td> | ||||
| 						<td> | ||||
| 							<a href="/completeTask?task={{.ID}}">Mark as completed</a> | ||||
| 						</td> | ||||
| 					</tr> | ||||
| 				{{end}} | ||||
| 						<tr> | ||||
| 							<td><label><input type="text" name="name" placeholder="Name"></label></td> | ||||
| 							<td></td> | ||||
| 							<td><label><input type="number" name="reward" placeholder="Reward"></label></td> | ||||
| 							<td><label><input type="text" name="schedule" placeholder="Schedule"></label></td> | ||||
| 							<td><input type="submit" value="Create"></td> | ||||
| 						</tr> | ||||
| 				</tbody> | ||||
| 			</table> | ||||
| 		</form> | ||||
|  | ||||
| 		<h2>History</h2> | ||||
| 		<table border="1"> | ||||
| 			<thead> | ||||
| 			<tr> | ||||
| 				<th>Timestamp</th> | ||||
| 				<th>Allowance</th> | ||||
| 			</tr> | ||||
| 			</thead> | ||||
| 			<tbody> | ||||
| 			{{range .History}} | ||||
| 				<tr> | ||||
| 					<td>{{.Timestamp}}</td> | ||||
| 					<td>{{.Allowance}}</td> | ||||
| 				</tr> | ||||
| 			{{end}} | ||||
| 			</tbody> | ||||
| 		</table> | ||||
| 	{{end}} | ||||
| {{end}} | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										120
									
								
								common/api.yaml
									
									
									
									
									
								
							
							
						
						| @@ -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. | ||||
| @@ -411,6 +409,59 @@ paths: | ||||
|         404: | ||||
|           description: The task could not be found. | ||||
|  | ||||
|   /api/transfer: | ||||
|     post: | ||||
|       summary: Transfer amount between allowances | ||||
|       requestBody: | ||||
|         required: true | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: | ||||
|               type: object | ||||
|               properties: | ||||
|                 from: | ||||
|                   type: integer | ||||
|                   description: Source allowance ID | ||||
|                 to: | ||||
|                   type: integer | ||||
|                   description: Destination allowance ID | ||||
|                 amount: | ||||
|                   type: number | ||||
|                   format: float | ||||
|                   description: Amount to transfer | ||||
|               required: | ||||
|                 - from | ||||
|                 - to | ||||
|                 - amount | ||||
|       responses: | ||||
|         '200': | ||||
|           description: Transfer successful | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 type: object | ||||
|                 properties: | ||||
|                   message: | ||||
|                     type: string | ||||
|         '400': | ||||
|           description: Invalid request | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 type: object | ||||
|                 properties: | ||||
|                   error: | ||||
|                     type: string | ||||
|         '404': | ||||
|           description: Allowance not found | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 type: object | ||||
|                 properties: | ||||
|                   error: | ||||
|                     type: string | ||||
|  | ||||
| components: | ||||
|   schemas: | ||||
|     task: | ||||
| @@ -424,7 +475,10 @@ components: | ||||
|           description: The task name | ||||
|         reward: | ||||
|           type: integer | ||||
|           description: The task reward, in cents | ||||
|           description: The task reward | ||||
|         schedule: | ||||
|           type: string | ||||
|           description: The schedule of the task, in cron format | ||||
|         assigned: | ||||
|           type: integer | ||||
|           description: The user ID of the user assigned to the task | ||||
|   | ||||
							
								
								
									
										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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | ||||
| { | ||||
|     "recommendations": [ | ||||
|       "ionic.ionic" | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										3
									
								
								frontend/allowance-planner-v2/.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| { | ||||
|   "typescript.preferences.autoImportFileExcludePatterns": ["@ionic/angular/common", "@ionic/angular/standalone"] | ||||
| } | ||||
							
								
								
									
										101
									
								
								frontend/allowance-planner-v2/android/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,101 @@ | ||||
| # Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore | ||||
|  | ||||
| # Built application files | ||||
| *.apk | ||||
| *.aar | ||||
| *.ap_ | ||||
| *.aab | ||||
|  | ||||
| # Files for the ART/Dalvik VM | ||||
| *.dex | ||||
|  | ||||
| # Java class files | ||||
| *.class | ||||
|  | ||||
| # Generated files | ||||
| bin/ | ||||
| gen/ | ||||
| out/ | ||||
| #  Uncomment the following line in case you need and you don't have the release build type files in your app | ||||
| # release/ | ||||
|  | ||||
| # Gradle files | ||||
| .gradle/ | ||||
| build/ | ||||
|  | ||||
| # Local configuration file (sdk path, etc) | ||||
| local.properties | ||||
|  | ||||
| # Proguard folder generated by Eclipse | ||||
| proguard/ | ||||
|  | ||||
| # Log Files | ||||
| *.log | ||||
|  | ||||
| # Android Studio Navigation editor temp files | ||||
| .navigation/ | ||||
|  | ||||
| # Android Studio captures folder | ||||
| captures/ | ||||
|  | ||||
| # IntelliJ | ||||
| *.iml | ||||
| .idea/workspace.xml | ||||
| .idea/tasks.xml | ||||
| .idea/gradle.xml | ||||
| .idea/assetWizardSettings.xml | ||||
| .idea/dictionaries | ||||
| .idea/libraries | ||||
| # Android Studio 3 in .gitignore file. | ||||
| .idea/caches | ||||
| .idea/modules.xml | ||||
| # Comment next line if keeping position of elements in Navigation Editor is relevant for you | ||||
| .idea/navEditor.xml | ||||
|  | ||||
| # Keystore files | ||||
| # Uncomment the following lines if you do not want to check your keystore files in. | ||||
| #*.jks | ||||
| #*.keystore | ||||
|  | ||||
| # External native build folder generated in Android Studio 2.2 and later | ||||
| .externalNativeBuild | ||||
| .cxx/ | ||||
|  | ||||
| # Google Services (e.g. APIs or Firebase) | ||||
| # google-services.json | ||||
|  | ||||
| # Freeline | ||||
| freeline.py | ||||
| freeline/ | ||||
| freeline_project_description.json | ||||
|  | ||||
| # fastlane | ||||
| fastlane/report.xml | ||||
| fastlane/Preview.html | ||||
| fastlane/screenshots | ||||
| fastlane/test_output | ||||
| fastlane/readme.md | ||||
|  | ||||
| # Version control | ||||
| vcs.xml | ||||
|  | ||||
| # lint | ||||
| lint/intermediates/ | ||||
| lint/generated/ | ||||
| lint/outputs/ | ||||
| lint/tmp/ | ||||
| # lint/reports/ | ||||
|  | ||||
| # Android Profiling | ||||
| *.hprof | ||||
|  | ||||
| # Cordova plugins for Capacitor | ||||
| capacitor-cordova-android-plugins | ||||
|  | ||||
| # Copied web assets | ||||
| app/src/main/assets/public | ||||
|  | ||||
| # Generated Config files | ||||
| app/src/main/assets/capacitor.config.json | ||||
| app/src/main/assets/capacitor.plugins.json | ||||
| app/src/main/res/xml/config.xml | ||||
							
								
								
									
										2
									
								
								frontend/allowance-planner-v2/android/app/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| /build/* | ||||
| !/build/.npmkeep | ||||
							
								
								
									
										54
									
								
								frontend/allowance-planner-v2/android/app/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,54 @@ | ||||
| apply plugin: 'com.android.application' | ||||
|  | ||||
| android { | ||||
|     namespace "io.ionic.starter" | ||||
|     compileSdk rootProject.ext.compileSdkVersion | ||||
|     defaultConfig { | ||||
|         applicationId "io.ionic.starter" | ||||
|         minSdkVersion rootProject.ext.minSdkVersion | ||||
|         targetSdkVersion rootProject.ext.targetSdkVersion | ||||
|         versionCode 1 | ||||
|         versionName "1.0" | ||||
|         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" | ||||
|         aaptOptions { | ||||
|              // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. | ||||
|              // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 | ||||
|             ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' | ||||
|         } | ||||
|     } | ||||
|     buildTypes { | ||||
|         release { | ||||
|             minifyEnabled false | ||||
|             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| repositories { | ||||
|     flatDir{ | ||||
|         dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' | ||||
|     } | ||||
| } | ||||
|  | ||||
| dependencies { | ||||
|     implementation fileTree(include: ['*.jar'], dir: 'libs') | ||||
|     implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" | ||||
|     implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" | ||||
|     implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" | ||||
|     implementation project(':capacitor-android') | ||||
|     testImplementation "junit:junit:$junitVersion" | ||||
|     androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" | ||||
|     androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" | ||||
|     implementation project(':capacitor-cordova-android-plugins') | ||||
| } | ||||
|  | ||||
| apply from: 'capacitor.build.gradle' | ||||
|  | ||||
| try { | ||||
|     def servicesJSON = file('google-services.json') | ||||
|     if (servicesJSON.text) { | ||||
|         apply plugin: 'com.google.gms.google-services' | ||||
|     } | ||||
| } catch(Exception e) { | ||||
|     logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") | ||||
| } | ||||
| @@ -0,0 +1,22 @@ | ||||
| // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN | ||||
|  | ||||
| android { | ||||
|   compileOptions { | ||||
|       sourceCompatibility JavaVersion.VERSION_21 | ||||
|       targetCompatibility JavaVersion.VERSION_21 | ||||
|   } | ||||
| } | ||||
|  | ||||
| apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" | ||||
| dependencies { | ||||
|     implementation project(':capacitor-app') | ||||
|     implementation project(':capacitor-haptics') | ||||
|     implementation project(':capacitor-keyboard') | ||||
|     implementation project(':capacitor-status-bar') | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| if (hasProperty('postBuildExtras')) { | ||||
|   postBuildExtras() | ||||
| } | ||||
							
								
								
									
										21
									
								
								frontend/allowance-planner-v2/android/app/proguard-rules.pro
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| # Add project specific ProGuard rules here. | ||||
| # You can control the set of applied configuration files using the | ||||
| # proguardFiles setting in build.gradle. | ||||
| # | ||||
| # For more details, see | ||||
| #   http://developer.android.com/guide/developing/tools/proguard.html | ||||
|  | ||||
| # If your project uses WebView with JS, uncomment the following | ||||
| # and specify the fully qualified class name to the JavaScript interface | ||||
| # class: | ||||
| #-keepclassmembers class fqcn.of.javascript.interface.for.webview { | ||||
| #   public *; | ||||
| #} | ||||
|  | ||||
| # Uncomment this to preserve the line number information for | ||||
| # debugging stack traces. | ||||
| #-keepattributes SourceFile,LineNumberTable | ||||
|  | ||||
| # If you keep the line number information, uncomment this to | ||||
| # hide the original source file name. | ||||
| #-renamesourcefileattribute SourceFile | ||||
| @@ -0,0 +1,26 @@ | ||||
| package com.getcapacitor.myapp; | ||||
|  | ||||
| import static org.junit.Assert.*; | ||||
|  | ||||
| import android.content.Context; | ||||
| import androidx.test.ext.junit.runners.AndroidJUnit4; | ||||
| import androidx.test.platform.app.InstrumentationRegistry; | ||||
| import org.junit.Test; | ||||
| import org.junit.runner.RunWith; | ||||
|  | ||||
| /** | ||||
|  * Instrumented test, which will execute on an Android device. | ||||
|  * | ||||
|  * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> | ||||
|  */ | ||||
| @RunWith(AndroidJUnit4.class) | ||||
| public class ExampleInstrumentedTest { | ||||
|  | ||||
|     @Test | ||||
|     public void useAppContext() throws Exception { | ||||
|         // Context of the app under test. | ||||
|         Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); | ||||
|  | ||||
|         assertEquals("com.getcapacitor.app", appContext.getPackageName()); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,41 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|  | ||||
|     <application | ||||
|         android:allowBackup="true" | ||||
|         android:icon="@mipmap/ic_launcher" | ||||
|         android:label="@string/app_name" | ||||
|         android:roundIcon="@mipmap/ic_launcher_round" | ||||
|         android:supportsRtl="true" | ||||
|         android:theme="@style/AppTheme"> | ||||
|  | ||||
|         <activity | ||||
|             android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation" | ||||
|             android:name=".MainActivity" | ||||
|             android:label="@string/title_activity_main" | ||||
|             android:theme="@style/AppTheme.NoActionBarLaunch" | ||||
|             android:launchMode="singleTask" | ||||
|             android:exported="true"> | ||||
|  | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.MAIN" /> | ||||
|                 <category android:name="android.intent.category.LAUNCHER" /> | ||||
|             </intent-filter> | ||||
|  | ||||
|         </activity> | ||||
|  | ||||
|         <provider | ||||
|             android:name="androidx.core.content.FileProvider" | ||||
|             android:authorities="${applicationId}.fileprovider" | ||||
|             android:exported="false" | ||||
|             android:grantUriPermissions="true"> | ||||
|             <meta-data | ||||
|                 android:name="android.support.FILE_PROVIDER_PATHS" | ||||
|                 android:resource="@xml/file_paths"></meta-data> | ||||
|         </provider> | ||||
|     </application> | ||||
|  | ||||
|     <!-- Permissions --> | ||||
|  | ||||
|     <uses-permission android:name="android.permission.INTERNET" /> | ||||
| </manifest> | ||||
| @@ -0,0 +1,5 @@ | ||||
| package io.ionic.starter; | ||||
|  | ||||
| import com.getcapacitor.BridgeActivity; | ||||
|  | ||||
| public class MainActivity extends BridgeActivity {} | ||||
| After Width: | Height: | Size: 33 KiB | 
| After Width: | Height: | Size: 9.1 KiB | 
| After Width: | Height: | Size: 16 KiB | 
| After Width: | Height: | Size: 64 KiB | 
| After Width: | Height: | Size: 91 KiB | 
| After Width: | Height: | Size: 122 KiB | 
| After Width: | Height: | Size: 32 KiB | 
| After Width: | Height: | Size: 9.1 KiB | 
| After Width: | Height: | Size: 16 KiB | 
| After Width: | Height: | Size: 65 KiB | 
| After Width: | Height: | Size: 90 KiB | 
| After Width: | Height: | Size: 119 KiB | 
| @@ -0,0 +1,34 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:aapt="http://schemas.android.com/aapt" | ||||
|     android:width="108dp" | ||||
|     android:height="108dp" | ||||
|     android:viewportHeight="108" | ||||
|     android:viewportWidth="108"> | ||||
|     <path | ||||
|         android:fillType="evenOdd" | ||||
|         android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z" | ||||
|         android:strokeColor="#00000000" | ||||
|         android:strokeWidth="1"> | ||||
|         <aapt:attr name="android:fillColor"> | ||||
|             <gradient | ||||
|                 android:endX="78.5885" | ||||
|                 android:endY="90.9159" | ||||
|                 android:startX="48.7653" | ||||
|                 android:startY="61.0927" | ||||
|                 android:type="linear"> | ||||
|                 <item | ||||
|                     android:color="#44000000" | ||||
|                     android:offset="0.0" /> | ||||
|                 <item | ||||
|                     android:color="#00000000" | ||||
|                     android:offset="1.0" /> | ||||
|             </gradient> | ||||
|         </aapt:attr> | ||||
|     </path> | ||||
|     <path | ||||
|         android:fillColor="#FFFFFF" | ||||
|         android:fillType="nonZero" | ||||
|         android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z" | ||||
|         android:strokeColor="#00000000" | ||||
|         android:strokeWidth="1" /> | ||||
| </vector> | ||||
| @@ -0,0 +1,170 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="108dp" | ||||
|     android:height="108dp" | ||||
|     android:viewportHeight="108" | ||||
|     android:viewportWidth="108"> | ||||
|     <path | ||||
|         android:fillColor="#26A69A" | ||||
|         android:pathData="M0,0h108v108h-108z" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M9,0L9,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,0L19,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M29,0L29,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M39,0L39,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M49,0L49,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M59,0L59,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M69,0L69,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M79,0L79,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M89,0L89,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M99,0L99,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,9L108,9" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,19L108,19" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,29L108,29" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,39L108,39" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,49L108,49" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,59L108,59" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,69L108,69" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,79L108,79" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,89L108,89" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,99L108,99" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,29L89,29" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,39L89,39" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,49L89,49" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,59L89,59" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,69L89,69" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,79L89,79" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M29,19L29,89" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M39,19L39,89" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M49,19L49,89" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M59,19L59,89" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M69,19L69,89" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M79,19L79,89" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
| </vector> | ||||
| After Width: | Height: | Size: 16 KiB | 
| @@ -0,0 +1,12 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     tools:context=".MainActivity"> | ||||
|  | ||||
|     <WebView | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" /> | ||||
| </androidx.coordinatorlayout.widget.CoordinatorLayout> | ||||
| @@ -0,0 +1,9 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <background> | ||||
|         <inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" /> | ||||
|     </background> | ||||
|     <foreground> | ||||
|         <inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" /> | ||||
|     </foreground> | ||||
| </adaptive-icon> | ||||
| @@ -0,0 +1,9 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <background> | ||||
|         <inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" /> | ||||
|     </background> | ||||
|     <foreground> | ||||
|         <inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" /> | ||||
|     </foreground> | ||||
| </adaptive-icon> | ||||
| After Width: | Height: | Size: 2.7 KiB | 
| After Width: | Height: | Size: 660 B | 
| After Width: | Height: | Size: 5.1 KiB | 
| After Width: | Height: | Size: 4.2 KiB | 
| After Width: | Height: | Size: 296 B | 
| After Width: | Height: | Size: 2.1 KiB | 
| After Width: | Height: | Size: 1.8 KiB | 
| After Width: | Height: | Size: 408 B | 
| After Width: | Height: | Size: 3.0 KiB | 
| After Width: | Height: | Size: 2.7 KiB | 
| After Width: | Height: | Size: 3.9 KiB | 
| After Width: | Height: | Size: 1006 B | 
| After Width: | Height: | Size: 7.5 KiB | 
| After Width: | Height: | Size: 6.4 KiB | 
| After Width: | Height: | Size: 6.5 KiB | 
| After Width: | Height: | Size: 1.8 KiB | 
| After Width: | Height: | Size: 13 KiB | 
| After Width: | Height: | Size: 10 KiB | 
| After Width: | Height: | Size: 9.2 KiB | 
| After Width: | Height: | Size: 2.5 KiB | 
| After Width: | Height: | Size: 18 KiB | 
| After Width: | Height: | Size: 16 KiB | 
| @@ -0,0 +1,4 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
|     <color name="ic_launcher_background">#FFFFFF</color> | ||||
| </resources> | ||||
| @@ -0,0 +1,7 @@ | ||||
| <?xml version='1.0' encoding='utf-8'?> | ||||
| <resources> | ||||
|     <string name="app_name">Allowance Planner V2</string> | ||||
|     <string name="title_activity_main">Allowance Planner V2</string> | ||||
|     <string name="package_name">io.ionic.starter</string> | ||||
|     <string name="custom_url_scheme">io.ionic.starter</string> | ||||
| </resources> | ||||
| @@ -0,0 +1,22 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
|  | ||||
|     <!-- Base application theme. --> | ||||
|     <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> | ||||
|         <!-- Customize your theme here. --> | ||||
|         <item name="colorPrimary">@color/colorPrimary</item> | ||||
|         <item name="colorPrimaryDark">@color/colorPrimaryDark</item> | ||||
|         <item name="colorAccent">@color/colorAccent</item> | ||||
|     </style> | ||||
|  | ||||
|     <style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar"> | ||||
|         <item name="windowActionBar">false</item> | ||||
|         <item name="windowNoTitle">true</item> | ||||
|         <item name="android:background">@null</item> | ||||
|     </style> | ||||
|  | ||||
|  | ||||
|     <style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen"> | ||||
|         <item name="android:background">@drawable/splash</item> | ||||
|     </style> | ||||
| </resources> | ||||
| @@ -0,0 +1,5 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <paths xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <external-path name="my_images" path="." /> | ||||
|     <cache-path name="my_cache_images" path="." /> | ||||
| </paths> | ||||
| @@ -0,0 +1,18 @@ | ||||
| package com.getcapacitor.myapp; | ||||
|  | ||||
| import static org.junit.Assert.*; | ||||
|  | ||||
| import org.junit.Test; | ||||
|  | ||||
| /** | ||||
|  * Example local unit test, which will execute on the development machine (host). | ||||
|  * | ||||
|  * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> | ||||
|  */ | ||||
| public class ExampleUnitTest { | ||||
|  | ||||
|     @Test | ||||
|     public void addition_isCorrect() throws Exception { | ||||
|         assertEquals(4, 2 + 2); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										29
									
								
								frontend/allowance-planner-v2/android/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,29 @@ | ||||
| // Top-level build file where you can add configuration options common to all sub-projects/modules. | ||||
|  | ||||
| buildscript { | ||||
|      | ||||
|     repositories { | ||||
|         google() | ||||
|         mavenCentral() | ||||
|     } | ||||
|     dependencies { | ||||
|         classpath 'com.android.tools.build:gradle:8.7.2' | ||||
|         classpath 'com.google.gms:google-services:4.4.2' | ||||
|  | ||||
|         // NOTE: Do not place your application dependencies here; they belong | ||||
|         // in the individual module build.gradle files | ||||
|     } | ||||
| } | ||||
|  | ||||
| apply from: "variables.gradle" | ||||
|  | ||||
| allprojects { | ||||
|     repositories { | ||||
|         google() | ||||
|         mavenCentral() | ||||
|     } | ||||
| } | ||||
|  | ||||
| task clean(type: Delete) { | ||||
|     delete rootProject.buildDir | ||||
| } | ||||
| @@ -0,0 +1,15 @@ | ||||
| // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN | ||||
| include ':capacitor-android' | ||||
| project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') | ||||
|  | ||||
| include ':capacitor-app' | ||||
| project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android') | ||||
|  | ||||
| include ':capacitor-haptics' | ||||
| project(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/haptics/android') | ||||
|  | ||||
| include ':capacitor-keyboard' | ||||
| project(':capacitor-keyboard').projectDir = new File('../node_modules/@capacitor/keyboard/android') | ||||
|  | ||||
| include ':capacitor-status-bar' | ||||
| project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android') | ||||
							
								
								
									
										22
									
								
								frontend/allowance-planner-v2/android/gradle.properties
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,22 @@ | ||||
| # Project-wide Gradle settings. | ||||
|  | ||||
| # IDE (e.g. Android Studio) users: | ||||
| # Gradle settings configured through the IDE *will override* | ||||
| # any settings specified in this file. | ||||
|  | ||||
| # For more details on how to configure your build environment visit | ||||
| # http://www.gradle.org/docs/current/userguide/build_environment.html | ||||
|  | ||||
| # Specifies the JVM arguments used for the daemon process. | ||||
| # The setting is particularly useful for tweaking memory settings. | ||||
| org.gradle.jvmargs=-Xmx1536m | ||||
|  | ||||
| # When configured, Gradle will run in incubating parallel mode. | ||||
| # This option should only be used with decoupled projects. More details, visit | ||||
| # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects | ||||
| # org.gradle.parallel=true | ||||
|  | ||||
| # AndroidX package structure to make it clearer which packages are bundled with the | ||||
| # Android operating system, and which are packaged with your app's APK | ||||
| # https://developer.android.com/topic/libraries/support-library/androidx-rn | ||||
| android.useAndroidX=true | ||||
							
								
								
									
										
											BIN
										
									
								
								frontend/allowance-planner-v2/android/gradle/wrapper/gradle-wrapper.jar
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										7
									
								
								frontend/allowance-planner-v2/android/gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | ||||
| distributionBase=GRADLE_USER_HOME | ||||
| distributionPath=wrapper/dists | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip | ||||
| networkTimeout=10000 | ||||
| validateDistributionUrl=true | ||||
| zipStoreBase=GRADLE_USER_HOME | ||||
| zipStorePath=wrapper/dists | ||||
							
								
								
									
										252
									
								
								frontend/allowance-planner-v2/android/gradlew
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,252 @@ | ||||
| #!/bin/sh | ||||
|  | ||||
| # | ||||
| # Copyright © 2015-2021 the original authors. | ||||
| # | ||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| # you may not use this file except in compliance with the License. | ||||
| # You may obtain a copy of the License at | ||||
| # | ||||
| #      https://www.apache.org/licenses/LICENSE-2.0 | ||||
| # | ||||
| # Unless required by applicable law or agreed to in writing, software | ||||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| # See the License for the specific language governing permissions and | ||||
| # limitations under the License. | ||||
| # | ||||
| # SPDX-License-Identifier: Apache-2.0 | ||||
| # | ||||
|  | ||||
| ############################################################################## | ||||
| # | ||||
| #   Gradle start up script for POSIX generated by Gradle. | ||||
| # | ||||
| #   Important for running: | ||||
| # | ||||
| #   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is | ||||
| #       noncompliant, but you have some other compliant shell such as ksh or | ||||
| #       bash, then to run this script, type that shell name before the whole | ||||
| #       command line, like: | ||||
| # | ||||
| #           ksh Gradle | ||||
| # | ||||
| #       Busybox and similar reduced shells will NOT work, because this script | ||||
| #       requires all of these POSIX shell features: | ||||
| #         * functions; | ||||
| #         * expansions «$var», «${var}», «${var:-default}», «${var+SET}», | ||||
| #           «${var#prefix}», «${var%suffix}», and «$( cmd )»; | ||||
| #         * compound commands having a testable exit status, especially «case»; | ||||
| #         * various built-in commands including «command», «set», and «ulimit». | ||||
| # | ||||
| #   Important for patching: | ||||
| # | ||||
| #   (2) This script targets any POSIX shell, so it avoids extensions provided | ||||
| #       by Bash, Ksh, etc; in particular arrays are avoided. | ||||
| # | ||||
| #       The "traditional" practice of packing multiple parameters into a | ||||
| #       space-separated string is a well documented source of bugs and security | ||||
| #       problems, so this is (mostly) avoided, by progressively accumulating | ||||
| #       options in "$@", and eventually passing that to Java. | ||||
| # | ||||
| #       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, | ||||
| #       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; | ||||
| #       see the in-line comments for details. | ||||
| # | ||||
| #       There are tweaks for specific operating systems such as AIX, CygWin, | ||||
| #       Darwin, MinGW, and NonStop. | ||||
| # | ||||
| #   (3) This script is generated from the Groovy template | ||||
| #       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt | ||||
| #       within the Gradle project. | ||||
| # | ||||
| #       You can find Gradle at https://github.com/gradle/gradle/. | ||||
| # | ||||
| ############################################################################## | ||||
|  | ||||
| # Attempt to set APP_HOME | ||||
|  | ||||
| # Resolve links: $0 may be a link | ||||
| app_path=$0 | ||||
|  | ||||
| # Need this for daisy-chained symlinks. | ||||
| while | ||||
|     APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path | ||||
|     [ -h "$app_path" ] | ||||
| do | ||||
|     ls=$( ls -ld "$app_path" ) | ||||
|     link=${ls#*' -> '} | ||||
|     case $link in             #( | ||||
|       /*)   app_path=$link ;; #( | ||||
|       *)    app_path=$APP_HOME$link ;; | ||||
|     esac | ||||
| done | ||||
|  | ||||
| # This is normally unused | ||||
| # shellcheck disable=SC2034 | ||||
| APP_BASE_NAME=${0##*/} | ||||
| # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) | ||||
| APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s | ||||
| ' "$PWD" ) || exit | ||||
|  | ||||
| # Use the maximum available, or set MAX_FD != -1 to use that value. | ||||
| MAX_FD=maximum | ||||
|  | ||||
| warn () { | ||||
|     echo "$*" | ||||
| } >&2 | ||||
|  | ||||
| die () { | ||||
|     echo | ||||
|     echo "$*" | ||||
|     echo | ||||
|     exit 1 | ||||
| } >&2 | ||||
|  | ||||
| # OS specific support (must be 'true' or 'false'). | ||||
| cygwin=false | ||||
| msys=false | ||||
| darwin=false | ||||
| nonstop=false | ||||
| case "$( uname )" in                #( | ||||
|   CYGWIN* )         cygwin=true  ;; #( | ||||
|   Darwin* )         darwin=true  ;; #( | ||||
|   MSYS* | MINGW* )  msys=true    ;; #( | ||||
|   NONSTOP* )        nonstop=true ;; | ||||
| esac | ||||
|  | ||||
| CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar | ||||
|  | ||||
|  | ||||
| # Determine the Java command to use to start the JVM. | ||||
| if [ -n "$JAVA_HOME" ] ; then | ||||
|     if [ -x "$JAVA_HOME/jre/sh/java" ] ; then | ||||
|         # IBM's JDK on AIX uses strange locations for the executables | ||||
|         JAVACMD=$JAVA_HOME/jre/sh/java | ||||
|     else | ||||
|         JAVACMD=$JAVA_HOME/bin/java | ||||
|     fi | ||||
|     if [ ! -x "$JAVACMD" ] ; then | ||||
|         die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME | ||||
|  | ||||
| Please set the JAVA_HOME variable in your environment to match the | ||||
| location of your Java installation." | ||||
|     fi | ||||
| else | ||||
|     JAVACMD=java | ||||
|     if ! command -v java >/dev/null 2>&1 | ||||
|     then | ||||
|         die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. | ||||
|  | ||||
| Please set the JAVA_HOME variable in your environment to match the | ||||
| location of your Java installation." | ||||
|     fi | ||||
| fi | ||||
|  | ||||
| # Increase the maximum file descriptors if we can. | ||||
| if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then | ||||
|     case $MAX_FD in #( | ||||
|       max*) | ||||
|         # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. | ||||
|         # shellcheck disable=SC2039,SC3045 | ||||
|         MAX_FD=$( ulimit -H -n ) || | ||||
|             warn "Could not query maximum file descriptor limit" | ||||
|     esac | ||||
|     case $MAX_FD in  #( | ||||
|       '' | soft) :;; #( | ||||
|       *) | ||||
|         # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. | ||||
|         # shellcheck disable=SC2039,SC3045 | ||||
|         ulimit -n "$MAX_FD" || | ||||
|             warn "Could not set maximum file descriptor limit to $MAX_FD" | ||||
|     esac | ||||
| fi | ||||
|  | ||||
| # Collect all arguments for the java command, stacking in reverse order: | ||||
| #   * args from the command line | ||||
| #   * the main class name | ||||
| #   * -classpath | ||||
| #   * -D...appname settings | ||||
| #   * --module-path (only if needed) | ||||
| #   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. | ||||
|  | ||||
| # For Cygwin or MSYS, switch paths to Windows format before running java | ||||
| if "$cygwin" || "$msys" ; then | ||||
|     APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) | ||||
|     CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) | ||||
|  | ||||
|     JAVACMD=$( cygpath --unix "$JAVACMD" ) | ||||
|  | ||||
|     # Now convert the arguments - kludge to limit ourselves to /bin/sh | ||||
|     for arg do | ||||
|         if | ||||
|             case $arg in                                #( | ||||
|               -*)   false ;;                            # don't mess with options #( | ||||
|               /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath | ||||
|                     [ -e "$t" ] ;;                      #( | ||||
|               *)    false ;; | ||||
|             esac | ||||
|         then | ||||
|             arg=$( cygpath --path --ignore --mixed "$arg" ) | ||||
|         fi | ||||
|         # Roll the args list around exactly as many times as the number of | ||||
|         # args, so each arg winds up back in the position where it started, but | ||||
|         # possibly modified. | ||||
|         # | ||||
|         # NB: a `for` loop captures its iteration list before it begins, so | ||||
|         # changing the positional parameters here affects neither the number of | ||||
|         # iterations, nor the values presented in `arg`. | ||||
|         shift                   # remove old arg | ||||
|         set -- "$@" "$arg"      # push replacement arg | ||||
|     done | ||||
| fi | ||||
|  | ||||
|  | ||||
| # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. | ||||
| DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' | ||||
|  | ||||
| # Collect all arguments for the java command: | ||||
| #   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, | ||||
| #     and any embedded shellness will be escaped. | ||||
| #   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be | ||||
| #     treated as '${Hostname}' itself on the command line. | ||||
|  | ||||
| set -- \ | ||||
|         "-Dorg.gradle.appname=$APP_BASE_NAME" \ | ||||
|         -classpath "$CLASSPATH" \ | ||||
|         org.gradle.wrapper.GradleWrapperMain \ | ||||
|         "$@" | ||||
|  | ||||
| # Stop when "xargs" is not available. | ||||
| if ! command -v xargs >/dev/null 2>&1 | ||||
| then | ||||
|     die "xargs is not available" | ||||
| fi | ||||
|  | ||||
| # Use "xargs" to parse quoted args. | ||||
| # | ||||
| # With -n1 it outputs one arg per line, with the quotes and backslashes removed. | ||||
| # | ||||
| # In Bash we could simply go: | ||||
| # | ||||
| #   readarray ARGS < <( xargs -n1 <<<"$var" ) && | ||||
| #   set -- "${ARGS[@]}" "$@" | ||||
| # | ||||
| # but POSIX shell has neither arrays nor command substitution, so instead we | ||||
| # post-process each arg (as a line of input to sed) to backslash-escape any | ||||
| # character that might be a shell metacharacter, then use eval to reverse | ||||
| # that process (while maintaining the separation between arguments), and wrap | ||||
| # the whole thing up as a single "set" statement. | ||||
| # | ||||
| # This will of course break if any of these variables contains a newline or | ||||
| # an unmatched quote. | ||||
| # | ||||
|  | ||||
| eval "set -- $( | ||||
|         printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | | ||||
|         xargs -n1 | | ||||
|         sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | | ||||
|         tr '\n' ' ' | ||||
|     )" '"$@"' | ||||
|  | ||||
| exec "$JAVACMD" "$@" | ||||
							
								
								
									
										94
									
								
								frontend/allowance-planner-v2/android/gradlew.bat
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,94 @@ | ||||
| @rem | ||||
| @rem Copyright 2015 the original author or authors. | ||||
| @rem | ||||
| @rem Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| @rem you may not use this file except in compliance with the License. | ||||
| @rem You may obtain a copy of the License at | ||||
| @rem | ||||
| @rem      https://www.apache.org/licenses/LICENSE-2.0 | ||||
| @rem | ||||
| @rem Unless required by applicable law or agreed to in writing, software | ||||
| @rem distributed under the License is distributed on an "AS IS" BASIS, | ||||
| @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| @rem See the License for the specific language governing permissions and | ||||
| @rem limitations under the License. | ||||
| @rem | ||||
| @rem SPDX-License-Identifier: Apache-2.0 | ||||
| @rem | ||||
|  | ||||
| @if "%DEBUG%"=="" @echo off | ||||
| @rem ########################################################################## | ||||
| @rem | ||||
| @rem  Gradle startup script for Windows | ||||
| @rem | ||||
| @rem ########################################################################## | ||||
|  | ||||
| @rem Set local scope for the variables with windows NT shell | ||||
| if "%OS%"=="Windows_NT" setlocal | ||||
|  | ||||
| set DIRNAME=%~dp0 | ||||
| if "%DIRNAME%"=="" set DIRNAME=. | ||||
| @rem This is normally unused | ||||
| set APP_BASE_NAME=%~n0 | ||||
| set APP_HOME=%DIRNAME% | ||||
|  | ||||
| @rem Resolve any "." and ".." in APP_HOME to make it shorter. | ||||
| for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi | ||||
|  | ||||
| @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. | ||||
| set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" | ||||
|  | ||||
| @rem Find java.exe | ||||
| if defined JAVA_HOME goto findJavaFromJavaHome | ||||
|  | ||||
| set JAVA_EXE=java.exe | ||||
| %JAVA_EXE% -version >NUL 2>&1 | ||||
| if %ERRORLEVEL% equ 0 goto execute | ||||
|  | ||||
| echo. 1>&2 | ||||
| echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 | ||||
| echo. 1>&2 | ||||
| echo Please set the JAVA_HOME variable in your environment to match the 1>&2 | ||||
| echo location of your Java installation. 1>&2 | ||||
|  | ||||
| goto fail | ||||
|  | ||||
| :findJavaFromJavaHome | ||||
| set JAVA_HOME=%JAVA_HOME:"=% | ||||
| set JAVA_EXE=%JAVA_HOME%/bin/java.exe | ||||
|  | ||||
| if exist "%JAVA_EXE%" goto execute | ||||
|  | ||||
| echo. 1>&2 | ||||
| echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 | ||||
| echo. 1>&2 | ||||
| echo Please set the JAVA_HOME variable in your environment to match the 1>&2 | ||||
| echo location of your Java installation. 1>&2 | ||||
|  | ||||
| goto fail | ||||
|  | ||||
| :execute | ||||
| @rem Setup the command line | ||||
|  | ||||
| set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar | ||||
|  | ||||
|  | ||||
| @rem Execute Gradle | ||||
| "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* | ||||
|  | ||||
| :end | ||||
| @rem End local scope for the variables with windows NT shell | ||||
| if %ERRORLEVEL% equ 0 goto mainEnd | ||||
|  | ||||
| :fail | ||||
| rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of | ||||
| rem the _cmd.exe /c_ return code! | ||||
| set EXIT_CODE=%ERRORLEVEL% | ||||
| if %EXIT_CODE% equ 0 set EXIT_CODE=1 | ||||
| if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% | ||||
| exit /b %EXIT_CODE% | ||||
|  | ||||
| :mainEnd | ||||
| if "%OS%"=="Windows_NT" endlocal | ||||
|  | ||||
| :omega | ||||
							
								
								
									
										5
									
								
								frontend/allowance-planner-v2/android/settings.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | ||||
| include ':app' | ||||
| include ':capacitor-cordova-android-plugins' | ||||
| project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') | ||||
|  | ||||
| apply from: 'capacitor.settings.gradle' | ||||
							
								
								
									
										16
									
								
								frontend/allowance-planner-v2/android/variables.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | ||||
| ext { | ||||
|     minSdkVersion = 23 | ||||
|     compileSdkVersion = 35 | ||||
|     targetSdkVersion = 35 | ||||
|     androidxActivityVersion = '1.9.2' | ||||
|     androidxAppCompatVersion = '1.7.0' | ||||
|     androidxCoordinatorLayoutVersion = '1.2.0' | ||||
|     androidxCoreVersion = '1.15.0' | ||||
|     androidxFragmentVersion = '1.8.4' | ||||
|     coreSplashScreenVersion = '1.0.1' | ||||
|     androidxWebkitVersion = '1.12.1' | ||||
|     junitVersion = '4.13.2' | ||||
|     androidxJunitVersion = '1.2.1' | ||||
|     androidxEspressoCoreVersion = '3.6.1' | ||||
|     cordovaAndroidVersion = '10.1.1' | ||||
| } | ||||
							
								
								
									
										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" | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								frontend/allowance-planner-v2/assets/icon-background.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 38 KiB | 
							
								
								
									
										
											BIN
										
									
								
								frontend/allowance-planner-v2/assets/icon-foreground.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 163 KiB | 
							
								
								
									
										
											BIN
										
									
								
								frontend/allowance-planner-v2/assets/splash.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 130 KiB | 
							
								
								
									
										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
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | ||||
| { | ||||
|   "name": "allowance-planner-v2", | ||||
|   "integrations": { | ||||
|     "capacitor": {} | ||||
|   }, | ||||
|   "type": "angular" | ||||
| } | ||||
							
								
								
									
										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 | ||||
|   }); | ||||
| }; | ||||
							
								
								
									
										21452
									
								
								frontend/allowance-planner-v2/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										70
									
								
								frontend/allowance-planner-v2/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,70 @@ | ||||
| { | ||||
|   "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/android": "7.2.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/assets": "^3.0.5", | ||||
|     "@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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| <ion-app> | ||||
|   <ion-router-outlet></ion-router-outlet> | ||||
| </ion-app> | ||||