Compare commits
	
		
			1 Commits
		
	
	
		
			main
			...
			4c68612ea0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4c68612ea0 | 
| @@ -1,8 +1,6 @@ | ||||
| name: Backend Deploy | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|   push: [main] | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
| @@ -19,9 +17,9 @@ jobs: | ||||
|       - name: Build | ||||
|         run: | | ||||
|           cd backend | ||||
|           docker build -t gitea.seeseepuff.be/seeseemelk/allowance-planner:$(git rev-parse --short HEAD) . | ||||
|           docker build -t gitea.seeseepuff.be/seeseemelk/wolproxy:$(git rev-parse --short HEAD) . | ||||
|  | ||||
|       - name: Push | ||||
|         run: | | ||||
|           cd backend | ||||
|           docker push gitea.seeseepuff.be/seeseemelk/allowance-planner:$(git rev-parse --short HEAD) | ||||
|           docker push gitea.seeseepuff.be/seeseemelk/wolproxy:$(git rev-parse --short HEAD) | ||||
|   | ||||
							
								
								
									
										16
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -14,19 +14,3 @@ In order to run the frontend, go to the `allowance-planner-v2` directory in the | ||||
| ```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 | ||||
| ``` | ||||
|   | ||||
							
								
								
									
										1
									
								
								backend/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,4 +1,3 @@ | ||||
| *.db3 | ||||
| *.db3-* | ||||
| *.db3.* | ||||
| /allowance_planner | ||||
|   | ||||
| @@ -2,23 +2,21 @@ package main | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"github.com/gavv/httpexpect/v2" | ||||
| 	"strconv" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gavv/httpexpect/v2" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	TestHistoryName = "Test History" | ||||
| 	TestAllowanceName = "Test History" | ||||
| ) | ||||
|  | ||||
| func startServer(t *testing.T) *httpexpect.Expect { | ||||
| 	config := ServerConfig{ | ||||
| 		Datasource: ":memory:", | ||||
| 		//Datasource: "test.db", | ||||
| 		Addr:    ":0", | ||||
| 		Started: make(chan bool), | ||||
| 		Addr:       ":0", | ||||
| 		Started:    make(chan bool), | ||||
| 	} | ||||
| 	go start(t.Context(), &config) | ||||
| 	<-config.Started | ||||
| @@ -64,7 +62,7 @@ func TestGetUserAllowance(t *testing.T) { | ||||
|  | ||||
| 	// Create a new allowance | ||||
| 	requestBody := map[string]interface{}{ | ||||
| 		"name":   TestHistoryName, | ||||
| 		"name":   TestAllowanceName, | ||||
| 		"target": 5000, | ||||
| 		"weight": 10, | ||||
| 	} | ||||
| @@ -75,7 +73,7 @@ func TestGetUserAllowance(t *testing.T) { | ||||
| 	result.Length().IsEqual(2) | ||||
| 	item := result.Value(1).Object() | ||||
| 	item.Value("id").IsEqual(1) | ||||
| 	item.Value("name").IsEqual(TestHistoryName) | ||||
| 	item.Value("name").IsEqual(TestAllowanceName) | ||||
| 	item.Value("target").IsEqual(5000) | ||||
| 	item.Value("weight").IsEqual(10) | ||||
| 	item.Value("progress").IsEqual(0) | ||||
| @@ -97,7 +95,7 @@ func TestCreateUserAllowance(t *testing.T) { | ||||
|  | ||||
| 	// Create a new allowance | ||||
| 	requestBody := map[string]interface{}{ | ||||
| 		"name":   TestHistoryName, | ||||
| 		"name":   TestAllowanceName, | ||||
| 		"target": 5000, | ||||
| 		"weight": 10, | ||||
| 	} | ||||
| @@ -122,7 +120,7 @@ func TestCreateUserAllowance(t *testing.T) { | ||||
|  | ||||
| 	allowance := allowances.Value(1).Object() | ||||
| 	allowance.Value("id").IsEqual(allowanceId) | ||||
| 	allowance.Value("name").IsEqual(TestHistoryName) | ||||
| 	allowance.Value("name").IsEqual(TestAllowanceName) | ||||
| 	allowance.Value("target").IsEqual(5000) | ||||
| 	allowance.Value("weight").IsEqual(10) | ||||
| 	allowance.Value("progress").IsEqual(0) | ||||
| @@ -132,7 +130,7 @@ func TestCreateUserAllowanceNoUser(t *testing.T) { | ||||
| 	e := startServer(t) | ||||
|  | ||||
| 	requestBody := map[string]interface{}{ | ||||
| 		"name":   TestHistoryName, | ||||
| 		"name":   TestAllowanceName, | ||||
| 		"target": 5000, | ||||
| 		"weight": 10, | ||||
| 	} | ||||
| @@ -173,7 +171,7 @@ func TestCreateUserAllowanceBadId(t *testing.T) { | ||||
| 	e := startServer(t) | ||||
|  | ||||
| 	requestBody := map[string]interface{}{ | ||||
| 		"name":   TestHistoryName, | ||||
| 		"name":   TestAllowanceName, | ||||
| 		"target": 5000, | ||||
| 		"weight": 10, | ||||
| 	} | ||||
| @@ -189,7 +187,7 @@ func TestDeleteUserAllowance(t *testing.T) { | ||||
|  | ||||
| 	// Create a new allowance to delete | ||||
| 	createRequest := map[string]interface{}{ | ||||
| 		"name":   TestHistoryName, | ||||
| 		"name":   TestAllowanceName, | ||||
| 		"target": 1000, | ||||
| 		"weight": 5, | ||||
| 	} | ||||
| @@ -286,54 +284,6 @@ func TestCreateTask(t *testing.T) { | ||||
| 	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) | ||||
|  | ||||
| @@ -484,50 +434,37 @@ func TestPutTaskInvalidTaskId(t *testing.T) { | ||||
| 	e.PUT("/task/999").WithJSON(requestBody).Expect().Status(404) | ||||
| } | ||||
|  | ||||
| func TestPostHistory(t *testing.T) { | ||||
| func TestPostAllowance(t *testing.T) { | ||||
| 	e := startServer(t) | ||||
|  | ||||
| 	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) | ||||
| 	e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 100}).Expect().Status(200) | ||||
| 	e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 20}).Expect().Status(200) | ||||
| 	e.POST("/user/1/history").WithJSON(PostHistory{Allowance: -10}).Expect().Status(200) | ||||
|  | ||||
| 	response := e.GET("/user/1").Expect().Status(200).JSON().Object() | ||||
| 	response.Value("allowance").Number().IsEqual(100 + 20 - 10) | ||||
| } | ||||
|  | ||||
| func TestPostHistoryInvalidUserId(t *testing.T) { | ||||
| func TestPostAllowanceInvalidUserId(t *testing.T) { | ||||
| 	e := startServer(t) | ||||
|  | ||||
| 	e.POST("/user/999/history").WithJSON(PostHistory{Allowance: 100, Description: "Good"}).Expect(). | ||||
| 	e.POST("/user/999/history").WithJSON(PostHistory{Allowance: 100}).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) | ||||
| 	e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 100}).Expect().Status(200) | ||||
| 	e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 20}).Expect().Status(200) | ||||
| 	e.POST("/user/1/history").WithJSON(PostHistory{Allowance: -10}).Expect().Status(200) | ||||
|  | ||||
| 	response := e.GET("/user/1/history").Expect().Status(200).JSON().Array() | ||||
| 	response.Length().IsEqual(3) | ||||
| 	response.Value(0).Object().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) { | ||||
| @@ -535,10 +472,9 @@ func TestGetUserAllowanceById(t *testing.T) { | ||||
|  | ||||
| 	// Create a new allowance | ||||
| 	requestBody := map[string]interface{}{ | ||||
| 		"name":   TestHistoryName, | ||||
| 		"name":   TestAllowanceName, | ||||
| 		"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()) | ||||
| @@ -546,21 +482,10 @@ func TestGetUserAllowanceById(t *testing.T) { | ||||
| 	// 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("name").IsEqual(TestAllowanceName) | ||||
| 	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) { | ||||
| @@ -588,10 +513,9 @@ func TestPutAllowanceById(t *testing.T) { | ||||
|  | ||||
| 	// Create a new allowance | ||||
| 	requestBody := map[string]interface{}{ | ||||
| 		"name":   TestHistoryName, | ||||
| 		"name":   TestAllowanceName, | ||||
| 		"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()) | ||||
| @@ -601,7 +525,6 @@ func TestPutAllowanceById(t *testing.T) { | ||||
| 		"name":   "Updated Allowance", | ||||
| 		"target": 6000, | ||||
| 		"weight": 15, | ||||
| 		"colour": "#3357FF", | ||||
| 	} | ||||
| 	e.PUT("/user/1/allowance/" + strconv.Itoa(allowanceId)).WithJSON(updateRequest).Expect().Status(200) | ||||
|  | ||||
| @@ -611,7 +534,6 @@ func TestPutAllowanceById(t *testing.T) { | ||||
| 	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) { | ||||
| @@ -668,36 +590,6 @@ func TestCompleteTask(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 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) | ||||
| @@ -736,11 +628,6 @@ func TestCompleteAllowance(t *testing.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) | ||||
|  | ||||
| @@ -753,15 +640,10 @@ func TestCompleteAllowance(t *testing.T) { | ||||
| 	// 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) { | ||||
| @@ -808,145 +690,6 @@ func TestPutBulkAllowance(t *testing.T) { | ||||
| 	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) | ||||
| @@ -964,88 +707,3 @@ func createTestAllowance(e *httpexpect.Expect, name string, target float64, weig | ||||
| 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) | ||||
| } | ||||
|   | ||||
| @@ -1,34 +0,0 @@ | ||||
| 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) | ||||
| } | ||||
| @@ -1,30 +0,0 @@ | ||||
| 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) | ||||
| } | ||||
							
								
								
									
										404
									
								
								backend/db.go
									
									
									
									
									
								
							
							
						
						| @@ -2,8 +2,6 @@ package main | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/adhocore/gronx" | ||||
| 	"log" | ||||
| 	"math" | ||||
| 	"time" | ||||
| @@ -85,14 +83,13 @@ func (db *Db) GetUserAllowances(userId int) ([]Allowance, error) { | ||||
| 	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 = ?"). | ||||
| 	for row := range db.db.Query("select id, name, target, balance, weight from allowances where user_id = ?"). | ||||
| 		Bind(userId).Range(&err) { | ||||
| 		allowance := Allowance{} | ||||
| 		var target, progress, colour int | ||||
| 		err = row.Scan(&allowance.ID, &allowance.Name, &target, &progress, &allowance.Weight, &colour) | ||||
| 		var target, progress int | ||||
| 		err = row.Scan(&allowance.ID, &allowance.Name, &target, &progress, &allowance.Weight) | ||||
| 		allowance.Target = float64(target) / 100.0 | ||||
| 		allowance.Progress = float64(progress) / 100.0 | ||||
| 		allowance.Colour = ConvertColourToString(colour) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| @@ -116,13 +113,11 @@ func (db *Db) GetUserAllowanceById(userId int, allowanceId int) (*Allowance, 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 = ?"). | ||||
| 		err := db.db.Query("select id, name, target, balance, weight from allowances where user_id = ? and id = ?"). | ||||
| 			Bind(userId, allowanceId). | ||||
| 			ScanSingle(&allowance.ID, &allowance.Name, &target, &progress, &allowance.Weight, &colour) | ||||
| 			ScanSingle(&allowance.ID, &allowance.Name, &target, &progress, &allowance.Weight) | ||||
| 		allowance.Target = float64(target) / 100.0 | ||||
| 		allowance.Progress = float64(progress) / 100.0 | ||||
| 		allowance.Colour = ConvertColourToString(colour) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| @@ -146,15 +141,9 @@ func (db *Db) CreateAllowance(userId int, allowance *CreateAllowanceRequest) (in | ||||
| 	} | ||||
| 	defer tx.MustRollback() | ||||
|  | ||||
| 	// 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). | ||||
| 	err = tx.Query("insert into allowances (user_id, name, target, weight) values (?, ?, ?, ?)"). | ||||
| 		Bind(userId, allowance.Name, int(math.Round(allowance.Target*100.0)), allowance.Weight). | ||||
| 		Exec() | ||||
|  | ||||
| 	if err != nil { | ||||
| @@ -208,9 +197,8 @@ func (db *Db) CompleteAllowance(userId int, allowanceId int) error { | ||||
|  | ||||
| 	// 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) | ||||
| 	err = tx.Query("select balance from allowances where id = ? and user_id = ?"). | ||||
| 		Bind(allowanceId, userId).ScanSingle(&cost) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @@ -223,8 +211,8 @@ func (db *Db) CompleteAllowance(userId int, allowanceId int) error { | ||||
| 	} | ||||
|  | ||||
| 	// 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)). | ||||
| 	err = tx.Query("insert into history (user_id, timestamp, amount) values (?, ?, ?)"). | ||||
| 		Bind(userId, time.Now().Unix(), -cost). | ||||
| 		Exec() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| @@ -267,14 +255,9 @@ func (db *Db) UpdateAllowance(userId int, allowanceId int, allowance *UpdateAllo | ||||
| 	} | ||||
| 	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). | ||||
| 	err = tx.Query("update allowances set name=?, target=?, weight=? where id = ? and user_id = ?"). | ||||
| 		Bind(allowance.Name, target, allowance.Weight, allowanceId, userId). | ||||
| 		Exec() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| @@ -314,20 +297,10 @@ 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 | ||||
| 	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). | ||||
| 	err = tx.Query("insert into tasks (name, reward, assigned) values (?, ?, ?)"). | ||||
| 		Bind(task.Name, reward, task.Assigned). | ||||
| 		Exec() | ||||
|  | ||||
| 	if err != nil { | ||||
| @@ -351,17 +324,13 @@ func (db *Db) CreateTask(task *CreateTaskRequest) (int, error) { | ||||
| } | ||||
|  | ||||
| func (db *Db) GetTasks() ([]Task, error) { | ||||
| 	err := db.UpdateScheduledTasks() | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to update scheduled tasks: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	tasks := make([]Task, 0) | ||||
| 	var err error | ||||
|  | ||||
| 	for row := range db.db.Query("select id, name, reward, assigned, schedule from tasks where completed is null").Range(&err) { | ||||
| 	for row := range db.db.Query("select id, name, reward, assigned from tasks").Range(&err) { | ||||
| 		task := Task{} | ||||
| 		var reward int64 | ||||
| 		err = row.Scan(&task.ID, &task.Name, &reward, &task.Assigned, &task.Schedule) | ||||
| 		err = row.Scan(&task.ID, &task.Name, &reward, &task.Assigned) | ||||
| 		task.Reward = float64(reward) / 100.0 | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| @@ -377,78 +346,16 @@ func (db *Db) GetTasks() ([]Task, error) { | ||||
| func (db *Db) GetTask(id int) (Task, error) { | ||||
| 	task := Task{} | ||||
|  | ||||
| 	err := db.UpdateScheduledTasks() | ||||
| 	if err != nil { | ||||
| 		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 | ||||
| 	} | ||||
| 	err := db.db.Query("select id, name, reward, assigned from tasks where id = ?"). | ||||
| 		Bind(id).ScanSingle(&task.ID, &task.Name, &reward, &task.Assigned) | ||||
| 	task.Reward = float64(reward) / 100.0 | ||||
| 	if err != nil { | ||||
| 		return Task{}, err | ||||
| 	} | ||||
| 	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 { | ||||
| @@ -499,28 +406,63 @@ func (db *Db) CompleteTask(taskId int) error { | ||||
| 	defer tx.MustRollback() | ||||
|  | ||||
| 	var reward int | ||||
| 	var rewardName string | ||||
| 	err = tx.Query("select reward, name from tasks where id = ?").Bind(taskId).ScanSingle(&reward, &rewardName) | ||||
| 	err = tx.Query("select reward from tasks where id = ?").Bind(taskId).ScanSingle(&reward) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	for userRow := range tx.Query("select id from users").Range(&err) { | ||||
| 	for userRow := range tx.Query("select id, weight from users").Range(&err) { | ||||
| 		var userId int | ||||
| 		err = userRow.Scan(&userId) | ||||
| 		var userWeight float64 | ||||
| 		err = userRow.Scan(&userId, &userWeight) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		// Add the history entry | ||||
| 		err = tx.Query("insert into history (user_id, timestamp, amount, description) values (?, ?, ?, ?)"). | ||||
| 			Bind(userId, time.Now().Unix(), reward, fmt.Sprintf("Task completed: %s", rewardName)). | ||||
| 		err = tx.Query("insert into history (user_id, timestamp, amount) values (?, ?, ?)"). | ||||
| 			Bind(userId, time.Now().Unix(), reward). | ||||
| 			Exec() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		err := db.addDistributedReward(tx, userId, reward) | ||||
| 		// 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() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| @@ -530,60 +472,11 @@ func (db *Db) CompleteTask(taskId int) error { | ||||
| 	} | ||||
|  | ||||
| 	// Remove the task | ||||
| 	err = tx.Query("update tasks set completed=? where id = ?").Bind(time.Now().Unix(), taskId).Exec() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	err = tx.Query("delete from tasks where id = ?").Bind(taskId).Exec() | ||||
|  | ||||
| 	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 { | ||||
| @@ -592,8 +485,8 @@ func (db *Db) AddHistory(userId int, allowance *PostHistory) error { | ||||
| 	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). | ||||
| 	err = tx.Query("insert into history (user_id, timestamp, amount) values (?, ?, ?)"). | ||||
| 		Bind(userId, time.Now().Unix(), amount). | ||||
| 		Exec() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| @@ -605,11 +498,11 @@ 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"). | ||||
| 	for row := range db.db.Query("select amount, `timestamp` from history where user_id = ? order by `timestamp` desc"). | ||||
| 		Bind(userId).Range(&err) { | ||||
| 		allowance := History{} | ||||
| 		var timestamp, amount int64 | ||||
| 		err = row.Scan(&amount, ×tamp, &allowance.Description) | ||||
| 		err = row.Scan(&amount, ×tamp) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| @@ -622,160 +515,3 @@ func (db *Db) GetHistory(userId int) ([]History, error) { | ||||
| 	} | ||||
| 	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() | ||||
| } | ||||
|   | ||||
| @@ -14,14 +14,12 @@ type UserWithAllowance struct { | ||||
| } | ||||
|  | ||||
| type History struct { | ||||
| 	Allowance   float64   `json:"allowance"` | ||||
| 	Timestamp   time.Time `json:"timestamp"` | ||||
| 	Description string    `json:"description"` | ||||
| 	Allowance float64   `json:"allowance"` | ||||
| 	Timestamp time.Time `json:"timestamp"` | ||||
| } | ||||
|  | ||||
| type PostHistory struct { | ||||
| 	Allowance   float64 `json:"allowance"` | ||||
| 	Description string  `json:"description"` | ||||
| 	Allowance float64 `json:"allowance"` | ||||
| } | ||||
|  | ||||
| // Task represents a task in the system. | ||||
| @@ -29,8 +27,7 @@ type Task struct { | ||||
| 	ID       int     `json:"id"` | ||||
| 	Name     string  `json:"name"` | ||||
| 	Reward   float64 `json:"reward"` | ||||
| 	Assigned *int    `json:"assigned"` | ||||
| 	Schedule *string `json:"schedule"` | ||||
| 	Assigned *int    `json:"assigned"` // Pointer to allow null | ||||
| } | ||||
|  | ||||
| type Allowance struct { | ||||
| @@ -39,21 +36,18 @@ type Allowance struct { | ||||
| 	Target   float64 `json:"target"` | ||||
| 	Progress float64 `json:"progress"` | ||||
| 	Weight   float64 `json:"weight"` | ||||
| 	Colour   string  `json:"colour"` | ||||
| } | ||||
|  | ||||
| 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 { | ||||
| @@ -69,20 +63,8 @@ type CreateTaskRequest struct { | ||||
| 	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,34 +3,30 @@ module allowance_planner | ||||
| go 1.24.2 | ||||
|  | ||||
| require ( | ||||
| 	gitea.seeseepuff.be/seeseemelk/mysqlite v0.15.0 | ||||
| 	github.com/adhocore/gronx v1.19.6 | ||||
| 	gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0 | ||||
| 	github.com/gavv/httpexpect/v2 v2.17.0 | ||||
| 	github.com/gin-contrib/cors v1.7.6 | ||||
| 	github.com/gin-gonic/gin v1.11.0 | ||||
| 	github.com/stretchr/testify v1.11.1 | ||||
| 	github.com/gin-contrib/cors v1.7.5 | ||||
| 	github.com/gin-gonic/gin v1.10.0 | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| 	github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect | ||||
| 	github.com/ajg/form v1.5.1 // 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/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/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.10 // indirect | ||||
| 	github.com/gabriel-vasile/mimetype v1.4.9 // 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.28.0 // indirect | ||||
| 	github.com/go-playground/validator/v10 v10.26.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 | ||||
| @@ -38,49 +34,44 @@ 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.3.0 // indirect | ||||
| 	github.com/klauspost/cpuid/v2 v2.2.10 // 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 v1.0.0 // indirect | ||||
| 	github.com/ncruces/go-strftime v0.1.9 // 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.4.0 // indirect | ||||
| 	github.com/sergi/go-diff v1.3.1 // indirect | ||||
| 	github.com/stretchr/testify v1.10.0 // indirect | ||||
| 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | ||||
| 	github.com/ugorji/go/codec v1.3.0 // indirect | ||||
| 	github.com/ugorji/go/codec v1.2.12 // indirect | ||||
| 	github.com/valyala/bytebufferpool v1.0.0 // indirect | ||||
| 	github.com/valyala/fasthttp v1.67.0 // indirect | ||||
| 	github.com/valyala/fasthttp v1.62.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 | ||||
| 	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 | ||||
| 	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 | ||||
| 	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.66.10 // indirect | ||||
| 	modernc.org/libc v1.65.7 // indirect | ||||
| 	modernc.org/mathutil v1.7.1 // indirect | ||||
| 	modernc.org/memory v1.11.0 // indirect | ||||
| 	modernc.org/sqlite v1.39.0 // indirect | ||||
| 	modernc.org/sqlite v1.37.0 // indirect | ||||
| 	moul.io/http2curl/v2 v2.3.0 // indirect | ||||
| 	zombiezen.com/go/sqlite v1.4.2 // indirect | ||||
| 	zombiezen.com/go/sqlite v1.4.0 // indirect | ||||
| ) | ||||
|   | ||||
							
								
								
									
										148
									
								
								backend/go.sum
									
									
									
									
									
								
							
							
						
						| @@ -1,21 +1,19 @@ | ||||
| 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= | ||||
| gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0 h1:aRItVfUj48fBmuec7rm/jY9KCfvHW2VzJfItVk4t8sw= | ||||
| gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE= | ||||
| github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4= | ||||
| github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= | ||||
| github.com/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.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/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/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= | ||||
| @@ -26,33 +24,31 @@ 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.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= | ||||
| github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= | ||||
| 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/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/cors v1.7.5 h1:cXC9SmofOrRg0w9PigwGlHG3ztswH6bqq4vJVXnvYMk= | ||||
| github.com/gin-contrib/cors v1.7.5/go.mod h1:4q3yi7xBEDDWKapjT2o1V7mScKDDr8k+jZ0fSquGoy0= | ||||
| github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= | ||||
| github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= | ||||
| github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= | ||||
| github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= | ||||
| 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/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.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= | ||||
| github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= | ||||
| 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/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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= | ||||
| github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= | ||||
| 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-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= | ||||
| @@ -70,8 +66,10 @@ 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.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= | ||||
| github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= | ||||
| 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/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= | ||||
| @@ -91,8 +89,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 v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= | ||||
| github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= | ||||
| 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/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= | ||||
| @@ -103,10 +101,6 @@ 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= | ||||
| @@ -114,28 +108,29 @@ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6po | ||||
| 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.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= | ||||
| github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= | ||||
| 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/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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= | ||||
| github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= | ||||
| 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/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.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= | ||||
| github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= | ||||
| 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/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.67.0 h1:tqKlJMUP6iuNG8hGjK/s9J4kadH7HLV4ijEcPGsezac= | ||||
| github.com/valyala/fasthttp v1.67.0/go.mod h1:qYSIpqt/0XNmShgo/8Aq8E3UYWVVwNS2QYmzd8WIEPM= | ||||
| 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/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= | ||||
| @@ -154,51 +149,49 @@ 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= | ||||
| 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/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= | ||||
| golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= | ||||
| 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.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/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/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.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= | ||||
| golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= | ||||
| 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/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.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= | ||||
| golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= | ||||
| 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/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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= | ||||
| golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= | ||||
| golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= | ||||
| golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= | ||||
| 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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= | ||||
| golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= | ||||
| golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= | ||||
| golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= | ||||
| 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.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= | ||||
| golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= | ||||
| 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/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.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= | ||||
| golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= | ||||
| golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= | ||||
| golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= | ||||
| 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.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= | ||||
| google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= | ||||
| google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= | ||||
| google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | ||||
| @@ -213,18 +206,16 @@ 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.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/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.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= | ||||
| modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= | ||||
| modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8= | ||||
| modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= | ||||
| modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= | ||||
| modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= | ||||
| modernc.org/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/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00= | ||||
| modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU= | ||||
| modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= | ||||
| modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= | ||||
| modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= | ||||
| @@ -233,13 +224,14 @@ 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.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY= | ||||
| modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= | ||||
| modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= | ||||
| modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= | ||||
| 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= | ||||
| zombiezen.com/go/sqlite v1.4.2 h1:KZXLrBuJ7tKNEm+VJcApLMeQbhmAUOKA5VWS93DfFRo= | ||||
| zombiezen.com/go/sqlite v1.4.2/go.mod h1:5Kd4taTAD4MkBzT25mQ9uaAlLjyR0rFhsR6iINO70jc= | ||||
| 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= | ||||
|   | ||||
							
								
								
									
										108
									
								
								backend/main.go
									
									
									
									
									
								
							
							
						
						| @@ -4,14 +4,13 @@ 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" | ||||
| ) | ||||
| @@ -25,8 +24,6 @@ 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. | ||||
| @@ -46,11 +43,6 @@ 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 { | ||||
| @@ -376,56 +368,6 @@ func completeAllowance(c *gin.Context) { | ||||
| 	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) { | ||||
| 	var taskRequest CreateTaskRequest | ||||
| 	if err := c.ShouldBindJSON(&taskRequest); err != nil { | ||||
| @@ -439,11 +381,6 @@ 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) | ||||
| @@ -521,11 +458,6 @@ 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 { | ||||
| @@ -607,11 +539,6 @@ func postHistory(c *gin.Context) { | ||||
| 		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) | ||||
| @@ -651,32 +578,6 @@ func getHistory(c *gin.Context) { | ||||
| 	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. | ||||
| @@ -705,14 +606,12 @@ func start(ctx context.Context, config *ServerConfig) { | ||||
| 	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.DELETE("/api/task/:taskId", deleteTask) | ||||
| 	router.POST("/api/task/:taskId/complete", completeTask) | ||||
| 	router.POST("/api/transfer", transfer) | ||||
|  | ||||
| 	srv := &http.Server{ | ||||
| 		Addr:    config.Addr, | ||||
| @@ -751,10 +650,5 @@ func main() { | ||||
| 		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) | ||||
| } | ||||
|   | ||||
| @@ -2,7 +2,7 @@ create table users | ||||
| ( | ||||
|     id   integer primary key, | ||||
|     name text not null, | ||||
|     weight real not null default 10.0, | ||||
|     weight real not null default 0.0, | ||||
|     balance integer not null default 0 | ||||
| ) strict; | ||||
|  | ||||
|   | ||||
| @@ -1,2 +0,0 @@ | ||||
| alter table allowances | ||||
| add column colour integer; | ||||
| @@ -1 +0,0 @@ | ||||
| update users set weight = 10.0 where weight = 0.0; | ||||
| @@ -1,2 +0,0 @@ | ||||
| alter table history | ||||
| add column description text; | ||||
| @@ -1,3 +0,0 @@ | ||||
| alter table tasks add column schedule text; | ||||
| alter table tasks add column completed date; | ||||
| alter table tasks add column next_run date; | ||||
| @@ -3,7 +3,6 @@ package main | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| ) | ||||
| @@ -27,22 +26,11 @@ func loadWebEndpoints(router *gin.Engine) { | ||||
| 	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) | ||||
| 		c.SetCookie("user", c.Query("user"), 3600, "/", "localhost", false, true) | ||||
| 	} | ||||
| 	redirectToPage(c, "/") | ||||
| 	c.Redirect(http.StatusFound, "/") | ||||
| } | ||||
|  | ||||
| func renderIndex(c *gin.Context) { | ||||
| @@ -71,24 +59,16 @@ func renderCreateTask(c *gin.Context) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	request := &CreateTaskRequest{ | ||||
| 	_, err = db.CreateTask(&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) | ||||
| 	c.Redirect(http.StatusFound, "/") | ||||
| } | ||||
|  | ||||
| func renderCompleteTask(c *gin.Context) { | ||||
| @@ -105,7 +85,7 @@ func renderCompleteTask(c *gin.Context) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	redirectToPageStatus(c, "/", http.StatusFound) | ||||
| 	c.Redirect(http.StatusFound, "/") | ||||
| } | ||||
|  | ||||
| func renderCreateAllowance(c *gin.Context) { | ||||
| @@ -142,7 +122,7 @@ func renderCreateAllowance(c *gin.Context) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	redirectToPageStatus(c, "/", http.StatusFound) | ||||
| 	c.Redirect(http.StatusFound, "/") | ||||
| } | ||||
|  | ||||
| func renderCompleteAllowance(c *gin.Context) { | ||||
| @@ -164,12 +144,11 @@ func renderCompleteAllowance(c *gin.Context) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	redirectToPageStatus(c, "/", http.StatusFound) | ||||
| 	c.Redirect(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 | ||||
| @@ -193,7 +172,7 @@ func getCurrentUser(c *gin.Context) *int { | ||||
|  | ||||
| func unsetUserCookie(c *gin.Context) { | ||||
| 	c.SetCookie("user", "", -1, "/", "localhost", false, true) | ||||
| 	redirectToPageStatus(c, "/", http.StatusFound) | ||||
| 	c.Redirect(http.StatusFound, "/") | ||||
| } | ||||
|  | ||||
| func renderNoUser(c *gin.Context) { | ||||
|   | ||||
| @@ -3,11 +3,9 @@ | ||||
| <head> | ||||
| 	<title>Allowance Planner 2000</title> | ||||
| 	<style> | ||||
| 		<!-- | ||||
| 		tr:hover { | ||||
| 			background-color: #f0f0f0; | ||||
| 		} | ||||
| 		--> | ||||
| 	</style> | ||||
| </head> | ||||
| <body> | ||||
| @@ -29,7 +27,7 @@ | ||||
| 	{{if ne .CurrentUser 0}} | ||||
| 		<h2>Allowances</h2> | ||||
| 		<form action="/createAllowance" method="post"> | ||||
| 			<table border=1> | ||||
| 			<table border="1"> | ||||
| 				<thead> | ||||
| 				<tr> | ||||
| 					<th>Name</th> | ||||
| @@ -45,7 +43,7 @@ | ||||
| 						<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> | ||||
| 						<td><button>Create</button></td> | ||||
| 					</tr> | ||||
| 				{{range .Allowances}} | ||||
| 					{{if eq .ID 0}} | ||||
| @@ -81,7 +79,6 @@ | ||||
| 					<th>Name</th> | ||||
| 					<th>Assigned</th> | ||||
| 					<th>Reward</th> | ||||
| 					<th>Schedule</th> | ||||
| 					<th>Actions</th> | ||||
| 				</tr> | ||||
| 				</thead> | ||||
| @@ -97,7 +94,6 @@ | ||||
| 							{{end}} | ||||
| 						</td> | ||||
| 						<td>{{.Reward}}</td> | ||||
| 						<td>{{.Schedule}}</td> | ||||
| 						<td> | ||||
| 							<a href="/completeTask?task={{.ID}}">Mark as completed</a> | ||||
| 						</td> | ||||
| @@ -107,8 +103,7 @@ | ||||
| 							<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> | ||||
| 							<td><button>Create</button></td> | ||||
| 						</tr> | ||||
| 				</tbody> | ||||
| 			</table> | ||||
|   | ||||
| @@ -409,59 +409,6 @@ 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: | ||||
| @@ -475,10 +422,7 @@ components: | ||||
|           description: The task name | ||||
|         reward: | ||||
|           type: integer | ||||
|           description: The task reward | ||||
|         schedule: | ||||
|           type: string | ||||
|           description: The schedule of the task, in cron format | ||||
|           description: The task reward, in cents | ||||
|         assigned: | ||||
|           type: integer | ||||
|           description: The user ID of the user assigned to the task | ||||
|   | ||||
							
								
								
									
										101
									
								
								frontend/allowance-planner-v2/android/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,101 +0,0 @@ | ||||
| # 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 | ||||
| @@ -1,2 +0,0 @@ | ||||
| /build/* | ||||
| !/build/.npmkeep | ||||
| @@ -1,54 +0,0 @@ | ||||
| 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") | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| // 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() | ||||
| } | ||||
| @@ -1,21 +0,0 @@ | ||||
| # 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 | ||||
| @@ -1,26 +0,0 @@ | ||||
| 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()); | ||||
|     } | ||||
| } | ||||
| @@ -1,41 +0,0 @@ | ||||
| <?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> | ||||
| @@ -1,5 +0,0 @@ | ||||
| package io.ionic.starter; | ||||
|  | ||||
| import com.getcapacitor.BridgeActivity; | ||||
|  | ||||
| public class MainActivity extends BridgeActivity {} | ||||
| Before Width: | Height: | Size: 33 KiB | 
| Before Width: | Height: | Size: 9.1 KiB | 
| Before Width: | Height: | Size: 16 KiB | 
| Before Width: | Height: | Size: 64 KiB | 
| Before Width: | Height: | Size: 91 KiB | 
| Before Width: | Height: | Size: 122 KiB | 
| Before Width: | Height: | Size: 32 KiB | 
| Before Width: | Height: | Size: 9.1 KiB | 
| Before Width: | Height: | Size: 16 KiB | 
| Before Width: | Height: | Size: 65 KiB | 
| Before Width: | Height: | Size: 90 KiB | 
| Before Width: | Height: | Size: 119 KiB | 
| @@ -1,34 +0,0 @@ | ||||
| <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> | ||||
| @@ -1,170 +0,0 @@ | ||||
| <?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> | ||||
| Before Width: | Height: | Size: 16 KiB | 
| @@ -1,12 +0,0 @@ | ||||
| <?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> | ||||
| @@ -1,9 +0,0 @@ | ||||
| <?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> | ||||
| @@ -1,9 +0,0 @@ | ||||
| <?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> | ||||
| Before Width: | Height: | Size: 2.7 KiB | 
| Before Width: | Height: | Size: 660 B | 
| Before Width: | Height: | Size: 5.1 KiB | 
| Before Width: | Height: | Size: 4.2 KiB | 
| Before Width: | Height: | Size: 296 B | 
| Before Width: | Height: | Size: 2.1 KiB | 
| Before Width: | Height: | Size: 1.8 KiB | 
| Before Width: | Height: | Size: 408 B | 
| Before Width: | Height: | Size: 3.0 KiB | 
| Before Width: | Height: | Size: 2.7 KiB | 
| Before Width: | Height: | Size: 3.9 KiB | 
| Before Width: | Height: | Size: 1006 B | 
| Before Width: | Height: | Size: 7.5 KiB | 
| Before Width: | Height: | Size: 6.4 KiB | 
| Before Width: | Height: | Size: 6.5 KiB | 
| Before Width: | Height: | Size: 1.8 KiB | 
| Before Width: | Height: | Size: 13 KiB | 
| Before Width: | Height: | Size: 10 KiB | 
| Before Width: | Height: | Size: 9.2 KiB | 
| Before Width: | Height: | Size: 2.5 KiB | 
| Before Width: | Height: | Size: 18 KiB | 
| Before Width: | Height: | Size: 16 KiB | 
| @@ -1,4 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
|     <color name="ic_launcher_background">#FFFFFF</color> | ||||
| </resources> | ||||
| @@ -1,7 +0,0 @@ | ||||
| <?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> | ||||
| @@ -1,22 +0,0 @@ | ||||
| <?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> | ||||
| @@ -1,5 +0,0 @@ | ||||
| <?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> | ||||
| @@ -1,18 +0,0 @@ | ||||
| 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); | ||||
|     } | ||||
| } | ||||
| @@ -1,29 +0,0 @@ | ||||
| // 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 | ||||
| } | ||||
| @@ -1,15 +0,0 @@ | ||||
| // 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') | ||||
| @@ -1,22 +0,0 @@ | ||||
| # 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 | ||||
| @@ -1,7 +0,0 @@ | ||||
| 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
									
									
								
							
							
						
						| @@ -1,252 +0,0 @@ | ||||
| #!/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" "$@" | ||||
| @@ -1,94 +0,0 @@ | ||||
| @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 | ||||
| @@ -1,5 +0,0 @@ | ||||
| include ':app' | ||||
| include ':capacitor-cordova-android-plugins' | ||||
| project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') | ||||
|  | ||||
| apply from: 'capacitor.settings.gradle' | ||||
| @@ -1,16 +0,0 @@ | ||||
| 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' | ||||
| } | ||||
| Before Width: | Height: | Size: 38 KiB | 
| Before Width: | Height: | Size: 163 KiB | 
| Before Width: | Height: | Size: 130 KiB | 
| @@ -2,7 +2,7 @@ import type { CapacitorConfig } from '@capacitor/cli'; | ||||
|  | ||||
| const config: CapacitorConfig = { | ||||
|   appId: 'io.ionic.starter', | ||||
|   appName: 'Allowance Planner V2', | ||||
|   appName: 'allowance-planner-v2', | ||||
|   webDir: 'www' | ||||
| }; | ||||
|  | ||||
|   | ||||
							
								
								
									
										3480
									
								
								frontend/allowance-planner-v2/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						| @@ -23,7 +23,6 @@ | ||||
|     "@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", | ||||
| @@ -47,7 +46,6 @@ | ||||
|     "@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", | ||||
|   | ||||
| @@ -11,6 +11,7 @@ const routes: Routes = [ | ||||
|     path: '', | ||||
|     loadChildren: () => import('./pages/tabs/tabs.module').then(m => m.TabsPageModule) | ||||
|   }, | ||||
|  | ||||
| ]; | ||||
| @NgModule({ | ||||
|   imports: [ | ||||
|   | ||||
| @@ -1,10 +0,0 @@ | ||||
| export interface Allowance { | ||||
|     id: number; | ||||
|     name: string; | ||||
|     target: number; | ||||
|     // Current allowance value | ||||
|     progress: number; | ||||
|     // Can be any positive number (backend checks for number relative to each other) | ||||
|     weight: number; | ||||
|     colour: string; | ||||
| } | ||||
| @@ -1,5 +0,0 @@ | ||||
| export interface History { | ||||
|     timestamp: string; | ||||
|     allowance: number; | ||||
|     description: string; | ||||
| } | ||||
| @@ -1,17 +0,0 @@ | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { Routes, RouterModule } from '@angular/router'; | ||||
|  | ||||
| import { AddAllowancePage } from './add-allowance.page'; | ||||
|  | ||||
| const routes: Routes = [ | ||||
|   { | ||||
|     path: '', | ||||
|     component: AddAllowancePage | ||||
|   } | ||||
| ]; | ||||
|  | ||||
| @NgModule({ | ||||
|   imports: [RouterModule.forChild(routes)], | ||||
|   exports: [RouterModule], | ||||
| }) | ||||
| export class AddAllowancePageRoutingModule {} | ||||
| @@ -1,23 +0,0 @@ | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { CommonModule } from '@angular/common'; | ||||
| import { FormsModule, ReactiveFormsModule } from '@angular/forms'; | ||||
|  | ||||
| import { IonicModule } from '@ionic/angular'; | ||||
|  | ||||
| import { AddAllowancePageRoutingModule } from './add-allowance-routing.module'; | ||||
|  | ||||
| import { AddAllowancePage } from './add-allowance.page'; | ||||
| import { MatIconModule } from '@angular/material/icon'; | ||||
|  | ||||
| @NgModule({ | ||||
|   imports: [ | ||||
|     CommonModule, | ||||
|     FormsModule, | ||||
|     IonicModule, | ||||
|     AddAllowancePageRoutingModule, | ||||
|     ReactiveFormsModule, | ||||
|     MatIconModule | ||||
|   ], | ||||
|   declarations: [AddAllowancePage] | ||||
| }) | ||||
| export class AddAllowancePageModule {} | ||||
| @@ -1,27 +0,0 @@ | ||||
| <ion-header [translucent]="true"> | ||||
|   <ion-toolbar> | ||||
|     <div class="toolbar"> | ||||
|       <div class="icon" (click)="navigateBack()"> | ||||
|         <mat-icon>arrow_back</mat-icon> | ||||
|       </div> | ||||
|       <ion-title *ngIf="isAddMode && goalId == 0">Add to Allowance</ion-title> | ||||
|       <ion-title *ngIf="isAddMode && goalId != 0">Add to Goal</ion-title> | ||||
|       <ion-title *ngIf="!isAddMode">Spend Allowance</ion-title> | ||||
|     </div> | ||||
|   </ion-toolbar> | ||||
| </ion-header> | ||||
|  | ||||
| <ion-content [fullscreen]="true"> | ||||
|   <form [formGroup]="form"> | ||||
|     <label>Amount</label> | ||||
|     <input id="amount" type="number" placeholder="0.00" name="price" min="0" value="0" step="0.01" formControlName="amount"/> | ||||
|  | ||||
|     <label>Description</label> | ||||
|     <input id="description" type="text" formControlName="description"/> | ||||
|  | ||||
|     <button type="button" [disabled]="!form.valid" (click)="changeAllowance()"> | ||||
|       <span *ngIf="isAddMode">Add</span> | ||||
|       <span *ngIf="!isAddMode">Spend</span> | ||||
|     </button> | ||||
|   </form> | ||||
| </ion-content> | ||||
| @@ -1,49 +0,0 @@ | ||||
| .toolbar { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
| } | ||||
|  | ||||
| .icon { | ||||
|     margin-left: 5px; | ||||
| } | ||||
|  | ||||
| form { | ||||
|     height: 100%; | ||||
| } | ||||
|  | ||||
| form, | ||||
| .item { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     align-items: center; | ||||
| } | ||||
|  | ||||
| input { | ||||
|     border: 1px solid var(--ion-color-primary); | ||||
|     border-radius: 5px; | ||||
|     width: 250px; | ||||
|     height: 40px; | ||||
|     padding-inline: 10px; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
| } | ||||
|  | ||||
| label { | ||||
|     color: var(--ion-color-primary); | ||||
|     margin-top: 25px; | ||||
|     margin-bottom: 10px; | ||||
| } | ||||
|  | ||||
| button { | ||||
|     background-color: var(--ion-color-primary); | ||||
|     border-radius: 5px; | ||||
|     color: white; | ||||
|     padding: 10px; | ||||
|     width: 250px; | ||||
|     margin-top: 100px; | ||||
| } | ||||
|  | ||||
| button:disabled, | ||||
| button[disabled]{ | ||||
|     opacity: 0.5; | ||||
| } | ||||
| @@ -1,17 +0,0 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
| import { AddAllowancePage } from './add-allowance.page'; | ||||
|  | ||||
| describe('AddAllowancePage', () => { | ||||
|   let component: AddAllowancePage; | ||||
|   let fixture: ComponentFixture<AddAllowancePage>; | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(AddAllowancePage); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -1,51 +0,0 @@ | ||||
| import { Location } from '@angular/common'; | ||||
| import { Component } from '@angular/core'; | ||||
| import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | ||||
| import { ActivatedRoute, Router } from '@angular/router'; | ||||
| import { AllowanceService } from 'src/app/services/allowance.service'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-add-allowance', | ||||
|   templateUrl: './add-allowance.page.html', | ||||
|   styleUrls: ['./add-allowance.page.scss'], | ||||
|   standalone: false, | ||||
| }) | ||||
| export class AddAllowancePage { | ||||
|   public form: FormGroup; | ||||
|   public goalId: number; | ||||
|   public userId: number; | ||||
|   public isAddMode = true; | ||||
| // Marcus' first comment | ||||
| //            b    ........a`.OK  ø¶Ópppppppp--P09OP | ||||
|  | ||||
|  | ||||
|   constructor( | ||||
|     private allowanceService: AllowanceService, | ||||
|     private route: ActivatedRoute, | ||||
|     private formBuilder: FormBuilder, | ||||
|     private router: Router, | ||||
|     private location: Location | ||||
|   ) { | ||||
|     this.userId = this.route.snapshot.params['id']; | ||||
|     this.goalId = this.route.snapshot.params['goalId']; | ||||
|  | ||||
|     this.form = this.formBuilder.group({ | ||||
|       amount: ['', Validators.required], | ||||
|       description: ['', Validators.required] | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   changeAllowance() { | ||||
|     this.allowanceService.addOrSpendAllowance( | ||||
|       this.goalId, | ||||
|       this.userId, | ||||
|       this.form.value.amount, | ||||
|       this.form.value.description | ||||
|     ); | ||||
|     this.router.navigate(['/tabs/allowance', this.userId]); | ||||
|   } | ||||
|  | ||||
|   navigateBack() { | ||||
|     this.location.back(); | ||||
|   } | ||||
| } | ||||
| @@ -1,17 +0,0 @@ | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { Routes, RouterModule } from '@angular/router'; | ||||
|  | ||||
| import { SpendllowancePage } from './spend-allowance.page'; | ||||
|  | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: '', | ||||
|         component: SpendllowancePage | ||||
|     } | ||||
| ]; | ||||
|  | ||||
| @NgModule({ | ||||
|     imports: [RouterModule.forChild(routes)], | ||||
|     exports: [RouterModule], | ||||
| }) | ||||
| export class SpendAllowancePageRoutingModule {} | ||||
| @@ -1,22 +0,0 @@ | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { CommonModule } from '@angular/common'; | ||||
| import { FormsModule, ReactiveFormsModule } from '@angular/forms'; | ||||
|  | ||||
| import { IonicModule } from '@ionic/angular'; | ||||
|  | ||||
| import { SpendAllowancePageRoutingModule } from './spend-allowance-routing.module'; | ||||
| import { SpendllowancePage } from './spend-allowance.page'; | ||||
| import { MatIconModule } from '@angular/material/icon'; | ||||
|  | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         CommonModule, | ||||
|         FormsModule, | ||||
|         IonicModule, | ||||
|         SpendAllowancePageRoutingModule, | ||||
|         ReactiveFormsModule, | ||||
|         MatIconModule | ||||
|     ], | ||||
|     declarations: [SpendllowancePage] | ||||
| }) | ||||
| export class SpendAllowancePageModule {} | ||||
| @@ -1,52 +0,0 @@ | ||||
| import { Location } from '@angular/common'; | ||||
| import { Component } from '@angular/core'; | ||||
| import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | ||||
| import { ActivatedRoute, Router } from '@angular/router'; | ||||
| import { AllowanceService } from 'src/app/services/allowance.service'; | ||||
|  | ||||
| @Component({ | ||||
|     selector: 'app-spend-allowance', | ||||
|     templateUrl: './add-allowance.page.html', | ||||
|     styleUrls: ['./add-allowance.page.scss'], | ||||
|     standalone: false, | ||||
| }) | ||||
| export class SpendllowancePage { | ||||
|     public form: FormGroup; | ||||
|     public goalId: number; | ||||
|     public userId: number; | ||||
|     public isAddMode = false; | ||||
|  | ||||
|     constructor( | ||||
|         private allowanceService: AllowanceService, | ||||
|         private route: ActivatedRoute, | ||||
|         private formBuilder: FormBuilder, | ||||
|         private router: Router, | ||||
|         private location: Location | ||||
|     ) { | ||||
|         this.userId = this.route.snapshot.params['id']; | ||||
|         this.goalId = this.route.snapshot.params['goalId']; | ||||
|  | ||||
|         this.form = this.formBuilder.group({ | ||||
|         amount: ['', Validators.required], | ||||
|         description: ['', Validators.required] | ||||
|         }); | ||||
|  | ||||
|         this.allowanceService.getAllowanceById(this.goalId, this.userId).subscribe(allowance => { | ||||
|             this.form.controls['amount'].addValidators([Validators.max(allowance.progress)]); | ||||
|         });  | ||||
|     } | ||||
|  | ||||
|     changeAllowance() { | ||||
|         this.allowanceService.addOrSpendAllowance( | ||||
|         this.goalId, | ||||
|         this.userId, | ||||
|         -this.form.value.amount, | ||||
|         this.form.value.description | ||||
|         ); | ||||
|         this.router.navigate(['/tabs/allowance', this.userId]); | ||||
|     } | ||||
|  | ||||
|     navigateBack() { | ||||
|         this.location.back(); | ||||
|     } | ||||
| } | ||||
| @@ -6,22 +6,6 @@ const routes: Routes = [ | ||||
|   { | ||||
|     path: ':id', | ||||
|     component: AllowancePage, | ||||
|   }, | ||||
|   { | ||||
|     path: ':id/add', | ||||
|     loadChildren: () => import('../edit-allowance/edit-allowance.module').then(m => m.EditAllowancePageModule) | ||||
|   }, | ||||
|   { | ||||
|     path: ':id/edit/:goalId', | ||||
|     loadChildren: () => import('../edit-allowance/edit-allowance.module').then(m => m.EditAllowancePageModule) | ||||
|   }, | ||||
|   { | ||||
|     path: ':id/increase/:goalId', | ||||
|     loadChildren: () => import('../add-allowance/add-allowance.module').then(m => m.AddAllowancePageModule) | ||||
|   }, | ||||
|   { | ||||
|     path: ':id/spend/:goalId', | ||||
|     loadChildren: () => import('../add-allowance/spend-allowance.module').then(m => m.SpendAllowancePageModule) | ||||
|   } | ||||
| ]; | ||||
|  | ||||
|   | ||||