Compare commits
	
		
			9 Commits
		
	
	
		
			schedules
			...
			2e28ac0dae
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 2e28ac0dae | |||
| 44e148fbb8 | |||
| 2b6dad709b | |||
| 885455454c | |||
| 5330cdd988 | |||
| 19292ec746 | |||
| 6a415ce878 | |||
| adff57bf29 | |||
| c4c2d42234 | 
| @@ -1,24 +0,0 @@ | |||||||
| name: Backend Build and Test |  | ||||||
| on: [push] |  | ||||||
|  |  | ||||||
| jobs: |  | ||||||
|   build: |  | ||||||
|     runs-on: standard-latest |  | ||||||
|     steps: |  | ||||||
|       - name: Checkout |  | ||||||
|         uses: actions/checkout@v4 |  | ||||||
|  |  | ||||||
|       - name: Setup Go |  | ||||||
|         uses: actions/setup-go@v5 |  | ||||||
|         with: |  | ||||||
|           go-version: '>=1.24' |  | ||||||
|  |  | ||||||
|       - name: Build |  | ||||||
|         run: | |  | ||||||
|           cd backend |  | ||||||
|           go build . |  | ||||||
|  |  | ||||||
|       - name: Test |  | ||||||
|         run: | |  | ||||||
|           cd backend |  | ||||||
|           go test . -v |  | ||||||
| @@ -1,27 +0,0 @@ | |||||||
| name: Backend Deploy |  | ||||||
| on: |  | ||||||
|   push: |  | ||||||
|     branches: |  | ||||||
|       - main |  | ||||||
|  |  | ||||||
| jobs: |  | ||||||
|   build: |  | ||||||
|     runs-on: standard-latest |  | ||||||
|     steps: |  | ||||||
|       - name: Checkout |  | ||||||
|         uses: actions/checkout@v4 |  | ||||||
|  |  | ||||||
|       - name: Login |  | ||||||
|         with: |  | ||||||
|           package_rw: ${{ secrets.PACKAGE_RW }} |  | ||||||
|         run: docker login gitea.seeseepuff.be -u seeseemelk -p ${{ secrets.PACKAGE_RW }} |  | ||||||
|  |  | ||||||
|       - name: Build |  | ||||||
|         run: | |  | ||||||
|           cd backend |  | ||||||
|           docker build -t gitea.seeseepuff.be/seeseemelk/allowance-planner:$(git rev-parse --short HEAD) . |  | ||||||
|  |  | ||||||
|       - name: Push |  | ||||||
|         run: | |  | ||||||
|           cd backend |  | ||||||
|           docker push gitea.seeseepuff.be/seeseemelk/allowance-planner:$(git rev-parse --short HEAD) |  | ||||||
							
								
								
									
										1
									
								
								backend/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,4 +1,3 @@ | |||||||
| *.db3 | *.db3 | ||||||
| *.db3-* | *.db3-* | ||||||
| *.db3.* |  | ||||||
| /allowance_planner | /allowance_planner | ||||||
|   | |||||||
| @@ -1,14 +0,0 @@ | |||||||
| FROM golang:1.24.2-alpine3.21 |  | ||||||
|  |  | ||||||
| WORKDIR /app |  | ||||||
| COPY go.mod go.sum ./ |  | ||||||
| RUN go mod download |  | ||||||
|  |  | ||||||
| COPY migrations ./migrations/ |  | ||||||
| COPY *.go ./ |  | ||||||
| COPY *.gohtml ./ |  | ||||||
| RUN go build -o /allowance_planner |  | ||||||
|  |  | ||||||
| EXPOSE 8080 |  | ||||||
| ENV GIN_MODE=release |  | ||||||
| CMD ["/allowance_planner"] |  | ||||||
| @@ -9,13 +9,12 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| 	TestHistoryName = "Test History" | 	TestAllowanceName = "Test History" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func startServer(t *testing.T) *httpexpect.Expect { | func startServer(t *testing.T) *httpexpect.Expect { | ||||||
| 	config := ServerConfig{ | 	config := ServerConfig{ | ||||||
| 		Datasource: ":memory:", | 		Datasource: ":memory:", | ||||||
| 		//Datasource: "test.db", |  | ||||||
| 		Addr:       ":0", | 		Addr:       ":0", | ||||||
| 		Started:    make(chan bool), | 		Started:    make(chan bool), | ||||||
| 	} | 	} | ||||||
| @@ -63,7 +62,7 @@ func TestGetUserAllowance(t *testing.T) { | |||||||
|  |  | ||||||
| 	// Create a new allowance | 	// Create a new allowance | ||||||
| 	requestBody := map[string]interface{}{ | 	requestBody := map[string]interface{}{ | ||||||
| 		"name":   TestHistoryName, | 		"name":   TestAllowanceName, | ||||||
| 		"target": 5000, | 		"target": 5000, | ||||||
| 		"weight": 10, | 		"weight": 10, | ||||||
| 	} | 	} | ||||||
| @@ -74,7 +73,7 @@ func TestGetUserAllowance(t *testing.T) { | |||||||
| 	result.Length().IsEqual(2) | 	result.Length().IsEqual(2) | ||||||
| 	item := result.Value(1).Object() | 	item := result.Value(1).Object() | ||||||
| 	item.Value("id").IsEqual(1) | 	item.Value("id").IsEqual(1) | ||||||
| 	item.Value("name").IsEqual(TestHistoryName) | 	item.Value("name").IsEqual(TestAllowanceName) | ||||||
| 	item.Value("target").IsEqual(5000) | 	item.Value("target").IsEqual(5000) | ||||||
| 	item.Value("weight").IsEqual(10) | 	item.Value("weight").IsEqual(10) | ||||||
| 	item.Value("progress").IsEqual(0) | 	item.Value("progress").IsEqual(0) | ||||||
| @@ -96,7 +95,7 @@ func TestCreateUserAllowance(t *testing.T) { | |||||||
|  |  | ||||||
| 	// Create a new allowance | 	// Create a new allowance | ||||||
| 	requestBody := map[string]interface{}{ | 	requestBody := map[string]interface{}{ | ||||||
| 		"name":   TestHistoryName, | 		"name":   TestAllowanceName, | ||||||
| 		"target": 5000, | 		"target": 5000, | ||||||
| 		"weight": 10, | 		"weight": 10, | ||||||
| 	} | 	} | ||||||
| @@ -121,7 +120,7 @@ func TestCreateUserAllowance(t *testing.T) { | |||||||
|  |  | ||||||
| 	allowance := allowances.Value(1).Object() | 	allowance := allowances.Value(1).Object() | ||||||
| 	allowance.Value("id").IsEqual(allowanceId) | 	allowance.Value("id").IsEqual(allowanceId) | ||||||
| 	allowance.Value("name").IsEqual(TestHistoryName) | 	allowance.Value("name").IsEqual(TestAllowanceName) | ||||||
| 	allowance.Value("target").IsEqual(5000) | 	allowance.Value("target").IsEqual(5000) | ||||||
| 	allowance.Value("weight").IsEqual(10) | 	allowance.Value("weight").IsEqual(10) | ||||||
| 	allowance.Value("progress").IsEqual(0) | 	allowance.Value("progress").IsEqual(0) | ||||||
| @@ -131,7 +130,7 @@ func TestCreateUserAllowanceNoUser(t *testing.T) { | |||||||
| 	e := startServer(t) | 	e := startServer(t) | ||||||
|  |  | ||||||
| 	requestBody := map[string]interface{}{ | 	requestBody := map[string]interface{}{ | ||||||
| 		"name":   TestHistoryName, | 		"name":   TestAllowanceName, | ||||||
| 		"target": 5000, | 		"target": 5000, | ||||||
| 		"weight": 10, | 		"weight": 10, | ||||||
| 	} | 	} | ||||||
| @@ -172,7 +171,7 @@ func TestCreateUserAllowanceBadId(t *testing.T) { | |||||||
| 	e := startServer(t) | 	e := startServer(t) | ||||||
|  |  | ||||||
| 	requestBody := map[string]interface{}{ | 	requestBody := map[string]interface{}{ | ||||||
| 		"name":   TestHistoryName, | 		"name":   TestAllowanceName, | ||||||
| 		"target": 5000, | 		"target": 5000, | ||||||
| 		"weight": 10, | 		"weight": 10, | ||||||
| 	} | 	} | ||||||
| @@ -188,7 +187,7 @@ func TestDeleteUserAllowance(t *testing.T) { | |||||||
|  |  | ||||||
| 	// Create a new allowance to delete | 	// Create a new allowance to delete | ||||||
| 	createRequest := map[string]interface{}{ | 	createRequest := map[string]interface{}{ | ||||||
| 		"name":   TestHistoryName, | 		"name":   TestAllowanceName, | ||||||
| 		"target": 1000, | 		"target": 1000, | ||||||
| 		"weight": 5, | 		"weight": 5, | ||||||
| 	} | 	} | ||||||
| @@ -285,54 +284,6 @@ func TestCreateTask(t *testing.T) { | |||||||
| 	responseWithUser.Value("id").Number().IsEqual(2) | 	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) { | func TestDeleteTask(t *testing.T) { | ||||||
| 	e := startServer(t) | 	e := startServer(t) | ||||||
|  |  | ||||||
| @@ -483,50 +434,37 @@ func TestPutTaskInvalidTaskId(t *testing.T) { | |||||||
| 	e.PUT("/task/999").WithJSON(requestBody).Expect().Status(404) | 	e.PUT("/task/999").WithJSON(requestBody).Expect().Status(404) | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestPostHistory(t *testing.T) { | func TestPostAllowance(t *testing.T) { | ||||||
| 	e := startServer(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: 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: 20}).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: -10}).Expect().Status(200) | ||||||
|  |  | ||||||
| 	response := e.GET("/user/1").Expect().Status(200).JSON().Object() | 	response := e.GET("/user/1").Expect().Status(200).JSON().Object() | ||||||
| 	response.Value("allowance").Number().IsEqual(100 + 20 - 10) | 	response.Value("allowance").Number().IsEqual(100 + 20 - 10) | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestPostHistoryInvalidUserId(t *testing.T) { | func TestPostAllowanceInvalidUserId(t *testing.T) { | ||||||
| 	e := startServer(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) | 		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) { | func TestGetHistory(t *testing.T) { | ||||||
| 	e := startServer(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: 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: 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: -10}).Expect().Status(200) | ||||||
|  |  | ||||||
| 	response := e.GET("/user/1/history").Expect().Status(200).JSON().Array() | 	response := e.GET("/user/1/history").Expect().Status(200).JSON().Array() | ||||||
| 	response.Length().IsEqual(3) | 	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("allowance").Number().IsEqual(100) | ||||||
| 	response.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0)) | 	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("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("allowance").Number().IsEqual(-10) | ||||||
| 	response.Value(2).Object().Value("description").String().IsEqual("Subtract 10") |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestGetUserAllowanceById(t *testing.T) { | func TestGetUserAllowanceById(t *testing.T) { | ||||||
| @@ -534,10 +472,9 @@ func TestGetUserAllowanceById(t *testing.T) { | |||||||
|  |  | ||||||
| 	// Create a new allowance | 	// Create a new allowance | ||||||
| 	requestBody := map[string]interface{}{ | 	requestBody := map[string]interface{}{ | ||||||
| 		"name":   TestHistoryName, | 		"name":   TestAllowanceName, | ||||||
| 		"target": 5000, | 		"target": 5000, | ||||||
| 		"weight": 10, | 		"weight": 10, | ||||||
| 		"colour": "#FF5733", |  | ||||||
| 	} | 	} | ||||||
| 	resp := e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201).JSON().Object() | 	resp := e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201).JSON().Object() | ||||||
| 	allowanceId := int(resp.Value("id").Number().Raw()) | 	allowanceId := int(resp.Value("id").Number().Raw()) | ||||||
| @@ -545,21 +482,10 @@ func TestGetUserAllowanceById(t *testing.T) { | |||||||
| 	// Retrieve the created allowance by ID | 	// Retrieve the created allowance by ID | ||||||
| 	result := e.GET("/user/1/allowance/" + strconv.Itoa(allowanceId)).Expect().Status(200).JSON().Object() | 	result := e.GET("/user/1/allowance/" + strconv.Itoa(allowanceId)).Expect().Status(200).JSON().Object() | ||||||
| 	result.Value("id").IsEqual(allowanceId) | 	result.Value("id").IsEqual(allowanceId) | ||||||
| 	result.Value("name").IsEqual(TestHistoryName) | 	result.Value("name").IsEqual(TestAllowanceName) | ||||||
| 	result.Value("target").IsEqual(5000) | 	result.Value("target").IsEqual(5000) | ||||||
| 	result.Value("weight").IsEqual(10) | 	result.Value("weight").IsEqual(10) | ||||||
| 	result.Value("progress").IsEqual(0) | 	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) { | func TestGetUserByAllowanceIdInvalidAllowance(t *testing.T) { | ||||||
| @@ -587,10 +513,9 @@ func TestPutAllowanceById(t *testing.T) { | |||||||
|  |  | ||||||
| 	// Create a new allowance | 	// Create a new allowance | ||||||
| 	requestBody := map[string]interface{}{ | 	requestBody := map[string]interface{}{ | ||||||
| 		"name":   TestHistoryName, | 		"name":   TestAllowanceName, | ||||||
| 		"target": 5000, | 		"target": 5000, | ||||||
| 		"weight": 10, | 		"weight": 10, | ||||||
| 		"colour": "#FF5733", |  | ||||||
| 	} | 	} | ||||||
| 	resp := e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201).JSON().Object() | 	resp := e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201).JSON().Object() | ||||||
| 	allowanceId := int(resp.Value("id").Number().Raw()) | 	allowanceId := int(resp.Value("id").Number().Raw()) | ||||||
| @@ -600,7 +525,6 @@ func TestPutAllowanceById(t *testing.T) { | |||||||
| 		"name":   "Updated Allowance", | 		"name":   "Updated Allowance", | ||||||
| 		"target": 6000, | 		"target": 6000, | ||||||
| 		"weight": 15, | 		"weight": 15, | ||||||
| 		"colour": "#3357FF", |  | ||||||
| 	} | 	} | ||||||
| 	e.PUT("/user/1/allowance/" + strconv.Itoa(allowanceId)).WithJSON(updateRequest).Expect().Status(200) | 	e.PUT("/user/1/allowance/" + strconv.Itoa(allowanceId)).WithJSON(updateRequest).Expect().Status(200) | ||||||
|  |  | ||||||
| @@ -610,7 +534,6 @@ func TestPutAllowanceById(t *testing.T) { | |||||||
| 	result.Value("name").IsEqual("Updated Allowance") | 	result.Value("name").IsEqual("Updated Allowance") | ||||||
| 	result.Value("target").IsEqual(6000) | 	result.Value("target").IsEqual(6000) | ||||||
| 	result.Value("weight").IsEqual(15) | 	result.Value("weight").IsEqual(15) | ||||||
| 	result.Value("colour").IsEqual("#3357FF") |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestCompleteTask(t *testing.T) { | func TestCompleteTask(t *testing.T) { | ||||||
| @@ -645,9 +568,9 @@ func TestCompleteTask(t *testing.T) { | |||||||
| 	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array() | 	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array() | ||||||
| 	allowances.Length().IsEqual(3) | 	allowances.Length().IsEqual(3) | ||||||
| 	allowances.Value(0).Object().Value("id").Number().IsEqual(0) | 	allowances.Value(0).Object().Value("id").Number().IsEqual(0) | ||||||
| 	allowances.Value(0).Object().Value("progress").Number().InDelta(30.34, 0.01) | 	allowances.Value(0).Object().Value("progress").Number().IsEqual(31) | ||||||
| 	allowances.Value(1).Object().Value("id").Number().IsEqual(1) | 	allowances.Value(1).Object().Value("id").Number().IsEqual(1) | ||||||
| 	allowances.Value(1).Object().Value("progress").Number().InDelta(60.66, 0.01) | 	allowances.Value(1).Object().Value("progress").Number().IsEqual(60) | ||||||
| 	allowances.Value(2).Object().Value("id").Number().IsEqual(2) | 	allowances.Value(2).Object().Value("id").Number().IsEqual(2) | ||||||
| 	allowances.Value(2).Object().Value("progress").Number().IsEqual(10) | 	allowances.Value(2).Object().Value("progress").Number().IsEqual(10) | ||||||
|  |  | ||||||
| @@ -667,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) { | func TestCompleteTaskAllowanceWeightsSumTo0(t *testing.T) { | ||||||
| 	e := startServer(t) | 	e := startServer(t) | ||||||
| 	taskId := createTestTaskWithAmount(e, 101) | 	taskId := createTestTaskWithAmount(e, 101) | ||||||
| @@ -735,11 +628,6 @@ func TestCompleteAllowance(t *testing.T) { | |||||||
| 	createTestTaskWithAmount(e, 100) | 	createTestTaskWithAmount(e, 100) | ||||||
| 	createTestAllowance(e, "Test Allowance 1", 100, 50) | 	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 | 	// Complete the task | ||||||
| 	e.POST("/task/1/complete").Expect().Status(200) | 	e.POST("/task/1/complete").Expect().Status(200) | ||||||
|  |  | ||||||
| @@ -752,15 +640,10 @@ func TestCompleteAllowance(t *testing.T) { | |||||||
| 	// Verify history is updated | 	// Verify history is updated | ||||||
| 	history := e.GET("/user/1/history").Expect().Status(200).JSON().Array() | 	history := e.GET("/user/1/history").Expect().Status(200).JSON().Array() | ||||||
| 	history.Length().IsEqual(2) | 	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("allowance").Number().IsEqual(100) | ||||||
| 	history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0)) | 	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("allowance").Number().IsEqual(-100) | ||||||
| 	history.Value(1).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0)) | 	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) { | func TestCompleteAllowanceInvalidUserId(t *testing.T) { | ||||||
| @@ -807,152 +690,13 @@ func TestPutBulkAllowance(t *testing.T) { | |||||||
| 	allowances.Value(2).Object().Value("weight").Number().IsEqual(10) | 	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) { | func getDelta(base time.Time, delta float64) (time.Time, time.Time) { | ||||||
| 	start := base.Add(-time.Duration(delta) * time.Second) | 	start := base.Add(-time.Duration(delta) * time.Second) | ||||||
| 	end := base.Add(time.Duration(delta) * time.Second) | 	end := base.Add(time.Duration(delta) * time.Second) | ||||||
| 	return start, end | 	return start, end | ||||||
| } | } | ||||||
|  |  | ||||||
| func createTestAllowance(e *httpexpect.Expect, name string, target float64, weight float64) { | func createTestAllowance(e *httpexpect.Expect, name string, target int, weight float64) { | ||||||
| 	e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{ | 	e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{ | ||||||
| 		Name:   name, | 		Name:   name, | ||||||
| 		Target: target, | 		Target: target, | ||||||
|   | |||||||
| @@ -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) |  | ||||||
| } |  | ||||||
							
								
								
									
										310
									
								
								backend/db.go
									
									
									
									
									
								
							
							
						
						| @@ -2,10 +2,7 @@ package main | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" |  | ||||||
| 	"github.com/adhocore/gronx" |  | ||||||
| 	"log" | 	"log" | ||||||
| 	"math" |  | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"gitea.seeseepuff.be/seeseemelk/mysqlite" | 	"gitea.seeseepuff.be/seeseemelk/mysqlite" | ||||||
| @@ -52,10 +49,8 @@ func (db *Db) GetUsers() ([]User, error) { | |||||||
| func (db *Db) GetUser(id int) (*UserWithAllowance, error) { | func (db *Db) GetUser(id int) (*UserWithAllowance, error) { | ||||||
| 	user := &UserWithAllowance{} | 	user := &UserWithAllowance{} | ||||||
|  |  | ||||||
| 	var allowance int |  | ||||||
| 	err := db.db.Query("select u.id, u.name, (select ifnull(sum(h.amount), 0) from history h where h.user_id = u.id) from users u where u.id = ?"). | 	err := db.db.Query("select u.id, u.name, (select ifnull(sum(h.amount), 0) from history h where h.user_id = u.id) from users u where u.id = ?"). | ||||||
| 		Bind(id).ScanSingle(&user.ID, &user.Name, &allowance) | 		Bind(id).ScanSingle(&user.ID, &user.Name, &user.Allowance) | ||||||
| 	user.Allowance = float64(allowance) / 100.0 |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| @@ -75,24 +70,18 @@ func (db *Db) UserExists(userId int) (bool, error) { | |||||||
| func (db *Db) GetUserAllowances(userId int) ([]Allowance, error) { | func (db *Db) GetUserAllowances(userId int) ([]Allowance, error) { | ||||||
| 	allowances := make([]Allowance, 0) | 	allowances := make([]Allowance, 0) | ||||||
| 	var err error | 	var err error | ||||||
| 	var progress int64 |  | ||||||
|  |  | ||||||
| 	totalAllowance := Allowance{} | 	totalAllowance := Allowance{} | ||||||
| 	err = db.db.Query("select balance, weight from users where id = ?").Bind(userId).ScanSingle(&progress, &totalAllowance.Weight) | 	err = db.db.Query("select balance, weight from users where id = ?").Bind(userId).ScanSingle(&totalAllowance.Progress, &totalAllowance.Weight) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	totalAllowance.Progress = float64(progress) / 100.0 |  | ||||||
| 	allowances = append(allowances, totalAllowance) | 	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) { | 		Bind(userId).Range(&err) { | ||||||
| 		allowance := Allowance{} | 		allowance := Allowance{} | ||||||
| 		var target, progress, colour int | 		err = row.Scan(&allowance.ID, &allowance.Name, &allowance.Target, &allowance.Progress, &allowance.Weight) | ||||||
| 		err = row.Scan(&allowance.ID, &allowance.Name, &target, &progress, &allowance.Weight, &colour) |  | ||||||
| 		allowance.Target = float64(target) / 100.0 |  | ||||||
| 		allowance.Progress = float64(progress) / 100.0 |  | ||||||
| 		allowance.Colour = ConvertColourToString(colour) |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| @@ -107,22 +96,15 @@ func (db *Db) GetUserAllowances(userId int) ([]Allowance, error) { | |||||||
| func (db *Db) GetUserAllowanceById(userId int, allowanceId int) (*Allowance, error) { | func (db *Db) GetUserAllowanceById(userId int, allowanceId int) (*Allowance, error) { | ||||||
| 	allowance := &Allowance{} | 	allowance := &Allowance{} | ||||||
| 	if allowanceId == 0 { | 	if allowanceId == 0 { | ||||||
| 		var progress int64 |  | ||||||
| 		err := db.db.Query("select balance, weight from users where id = ?"). | 		err := db.db.Query("select balance, weight from users where id = ?"). | ||||||
| 			Bind(userId).ScanSingle(&progress, &allowance.Weight) | 			Bind(userId).ScanSingle(&allowance.Progress, &allowance.Weight) | ||||||
| 		allowance.Progress = float64(progress) / 100.0 |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		var target, progress int64 | 		err := db.db.Query("select id, name, target, balance, weight from allowances where user_id = ? and id = ?"). | ||||||
| 		var colour int |  | ||||||
| 		err := db.db.Query("select id, name, target, balance, weight, colour from allowances where user_id = ? and id = ?"). |  | ||||||
| 			Bind(userId, allowanceId). | 			Bind(userId, allowanceId). | ||||||
| 			ScanSingle(&allowance.ID, &allowance.Name, &target, &progress, &allowance.Weight, &colour) | 			ScanSingle(&allowance.ID, &allowance.Name, &allowance.Target, &allowance.Progress, &allowance.Weight) | ||||||
| 		allowance.Target = float64(target) / 100.0 |  | ||||||
| 		allowance.Progress = float64(progress) / 100.0 |  | ||||||
| 		allowance.Colour = ConvertColourToString(colour) |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| @@ -146,15 +128,9 @@ func (db *Db) CreateAllowance(userId int, allowance *CreateAllowanceRequest) (in | |||||||
| 	} | 	} | ||||||
| 	defer tx.MustRollback() | 	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 | 	// Insert the new allowance | ||||||
| 	err = tx.Query("insert into allowances (user_id, name, target, weight, colour) values (?, ?, ?, ?, ?)"). | 	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, colour). | 		Bind(userId, allowance.Name, allowance.Target, allowance.Weight). | ||||||
| 		Exec() | 		Exec() | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -208,9 +184,8 @@ func (db *Db) CompleteAllowance(userId int, allowanceId int) error { | |||||||
|  |  | ||||||
| 	// Get the cost of the allowance | 	// Get the cost of the allowance | ||||||
| 	var cost int | 	var cost int | ||||||
| 	var allowanceName string | 	err = tx.Query("select balance from allowances where id = ? and user_id = ?"). | ||||||
| 	err = tx.Query("select balance, name from allowances where id = ? and user_id = ?"). | 		Bind(allowanceId, userId).ScanSingle(&cost) | ||||||
| 		Bind(allowanceId, userId).ScanSingle(&cost, &allowanceName) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @@ -223,8 +198,8 @@ func (db *Db) CompleteAllowance(userId int, allowanceId int) error { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Add a history entry | 	// Add a history entry | ||||||
| 	err = tx.Query("insert into history (user_id, timestamp, amount, description) values (?, ?, ?, ?)"). | 	err = tx.Query("insert into history (user_id, timestamp, amount) values (?, ?, ?)"). | ||||||
| 		Bind(userId, time.Now().Unix(), -cost, fmt.Sprintf("Allowance completed: %s", allowanceName)). | 		Bind(userId, time.Now().Unix(), -cost). | ||||||
| 		Exec() | 		Exec() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| @@ -267,14 +242,8 @@ func (db *Db) UpdateAllowance(userId int, allowanceId int, allowance *UpdateAllo | |||||||
| 	} | 	} | ||||||
| 	defer tx.MustRollback() | 	defer tx.MustRollback() | ||||||
|  |  | ||||||
| 	colour, err := ConvertStringToColour(allowance.Colour) | 	err = tx.Query("update allowances set name=?, target=?, weight=? where id = ? and user_id = ?"). | ||||||
| 	if err != nil { | 		Bind(allowance.Name, allowance.Target, allowance.Weight, allowanceId, userId). | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	target := int(math.Round(allowance.Target * 100.0)) |  | ||||||
| 	err = tx.Query("update allowances set name=?, target=?, weight=?, colour=? where id = ? and user_id = ?"). |  | ||||||
| 		Bind(allowance.Name, target, allowance.Weight, colour, allowanceId, userId). |  | ||||||
| 		Exec() | 		Exec() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| @@ -314,20 +283,9 @@ func (db *Db) CreateTask(task *CreateTaskRequest) (int, error) { | |||||||
| 	} | 	} | ||||||
| 	defer tx.MustRollback() | 	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 | 	// Insert the new task | ||||||
| 	reward := int(math.Round(task.Reward * 100.0)) | 	err = tx.Query("insert into tasks (name, reward, assigned) values (?, ?, ?)"). | ||||||
| 	err = tx.Query("insert into tasks (name, reward, assigned, schedule, next_run) values (?, ?, ?, ?, ?)"). | 		Bind(task.Name, task.Reward, task.Assigned). | ||||||
| 		Bind(task.Name, reward, task.Assigned, task.Schedule, nextRun). |  | ||||||
| 		Exec() | 		Exec() | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -351,18 +309,12 @@ func (db *Db) CreateTask(task *CreateTaskRequest) (int, error) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (db *Db) GetTasks() ([]Task, 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) | 	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{} | 		task := Task{} | ||||||
| 		var reward int64 | 		err = row.Scan(&task.ID, &task.Name, &task.Reward, &task.Assigned) | ||||||
| 		err = row.Scan(&task.ID, &task.Name, &reward, &task.Assigned, &task.Schedule) |  | ||||||
| 		task.Reward = float64(reward) / 100.0 |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| @@ -377,78 +329,14 @@ func (db *Db) GetTasks() ([]Task, error) { | |||||||
| func (db *Db) GetTask(id int) (Task, error) { | func (db *Db) GetTask(id int) (Task, error) { | ||||||
| 	task := Task{} | 	task := Task{} | ||||||
|  |  | ||||||
| 	err := db.UpdateScheduledTasks() | 	err := db.db.Query("select id, name, reward, assigned from tasks where id = ?"). | ||||||
|  | 		Bind(id).ScanSingle(&task.ID, &task.Name, &task.Reward, &task.Assigned) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return Task{}, fmt.Errorf("failed to update scheduled tasks: %w", err) | 		return Task{}, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var reward int64 |  | ||||||
| 	err = db.db.Query("select id, name, reward, assigned, schedule from tasks where id = ? and completed is null"). |  | ||||||
| 		Bind(id).ScanSingle(&task.ID, &task.Name, &reward, &task.Assigned, &task.Schedule) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return task, err |  | ||||||
| 	} |  | ||||||
| 	task.Reward = float64(reward) / 100.0 |  | ||||||
| 	return task, nil | 	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 { | func (db *Db) DeleteTask(id int) error { | ||||||
| 	tx, err := db.db.Begin() | 	tx, err := db.db.Begin() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -481,9 +369,8 @@ func (db *Db) UpdateTask(id int, task *CreateTaskRequest) error { | |||||||
| 	} | 	} | ||||||
| 	defer tx.MustRollback() | 	defer tx.MustRollback() | ||||||
|  |  | ||||||
| 	reward := int(math.Round(task.Reward * 100.0)) |  | ||||||
| 	err = tx.Query("update tasks set name=?, reward=?, assigned=? where id = ?"). | 	err = tx.Query("update tasks set name=?, reward=?, assigned=? where id = ?"). | ||||||
| 		Bind(task.Name, reward, task.Assigned, id). | 		Bind(task.Name, task.Reward, task.Assigned, id). | ||||||
| 		Exec() | 		Exec() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| @@ -499,52 +386,27 @@ func (db *Db) CompleteTask(taskId int) error { | |||||||
| 	defer tx.MustRollback() | 	defer tx.MustRollback() | ||||||
|  |  | ||||||
| 	var reward int | 	var reward int | ||||||
| 	var rewardName string | 	err = tx.Query("select reward from tasks where id = ?").Bind(taskId).ScanSingle(&reward) | ||||||
| 	err = tx.Query("select reward, name from tasks where id = ?").Bind(taskId).ScanSingle(&reward, &rewardName) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		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 | 		var userId int | ||||||
| 		err = userRow.Scan(&userId) | 		var userWeight float64 | ||||||
|  | 		err = userRow.Scan(&userId, &userWeight) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Add the history entry | 		// Add the history entry | ||||||
| 		err = tx.Query("insert into history (user_id, timestamp, amount, description) values (?, ?, ?, ?)"). | 		err = tx.Query("insert into history (user_id, timestamp, amount) values (?, ?, ?)"). | ||||||
| 			Bind(userId, time.Now().Unix(), reward, fmt.Sprintf("Task completed: %s", rewardName)). | 			Bind(userId, time.Now().Unix(), reward). | ||||||
| 			Exec() | 			Exec() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		err := db.addDistributedReward(tx, userId, reward) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Remove the task |  | ||||||
| 	err = tx.Query("update tasks set completed=? where id = ?").Bind(time.Now().Unix(), taskId).Exec() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return tx.Commit() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (db *Db) addDistributedReward(tx *mysqlite.Tx, userId int, reward int) error { |  | ||||||
| 	var userWeight float64 |  | ||||||
| 	err := tx.Query("select weight from users where id = ?").Bind(userId).ScanSingle(&userWeight) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 		// Calculate the sums of all weights | 		// Calculate the sums of all weights | ||||||
| 		var sumOfWeights float64 | 		var sumOfWeights float64 | ||||||
| 		err = tx.Query("select sum(weight) from allowances where user_id = ? and weight > 0").Bind(userId).ScanSingle(&sumOfWeights) | 		err = tx.Query("select sum(weight) from allowances where user_id = ? and weight > 0").Bind(userId).ScanSingle(&sumOfWeights) | ||||||
| @@ -581,7 +443,18 @@ func (db *Db) addDistributedReward(tx *mysqlite.Tx, userId int, reward int) erro | |||||||
| 		// Add the remaining reward to the user | 		// Add the remaining reward to the user | ||||||
| 		err = tx.Query("update users set balance = balance + ? where id = ?"). | 		err = tx.Query("update users set balance = balance + ? where id = ?"). | ||||||
| 			Bind(remainingReward, userId).Exec() | 			Bind(remainingReward, userId).Exec() | ||||||
|  | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Remove the task | ||||||
|  | 	err = tx.Query("delete from tasks where id = ?").Bind(taskId).Exec() | ||||||
|  |  | ||||||
|  | 	return tx.Commit() | ||||||
| } | } | ||||||
|  |  | ||||||
| func (db *Db) AddHistory(userId int, allowance *PostHistory) error { | func (db *Db) AddHistory(userId int, allowance *PostHistory) error { | ||||||
| @@ -591,9 +464,8 @@ func (db *Db) AddHistory(userId int, allowance *PostHistory) error { | |||||||
| 	} | 	} | ||||||
| 	defer tx.MustRollback() | 	defer tx.MustRollback() | ||||||
|  |  | ||||||
| 	amount := int(math.Round(allowance.Allowance * 100.0)) | 	err = tx.Query("insert into history (user_id, timestamp, amount) values (?, ?, ?)"). | ||||||
| 	err = tx.Query("insert into history (user_id, timestamp, amount, description) values (?, ?, ?, ?)"). | 		Bind(userId, time.Now().Unix(), allowance.Allowance). | ||||||
| 		Bind(userId, time.Now().Unix(), amount, allowance.Description). |  | ||||||
| 		Exec() | 		Exec() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| @@ -605,15 +477,14 @@ func (db *Db) GetHistory(userId int) ([]History, error) { | |||||||
| 	history := make([]History, 0) | 	history := make([]History, 0) | ||||||
| 	var err error | 	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) { | 		Bind(userId).Range(&err) { | ||||||
| 		allowance := History{} | 		allowance := History{} | ||||||
| 		var timestamp, amount int64 | 		var timestamp int64 | ||||||
| 		err = row.Scan(&amount, ×tamp, &allowance.Description) | 		err = row.Scan(&allowance.Allowance, ×tamp) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 		allowance.Allowance = float64(amount) / 100.0 |  | ||||||
| 		allowance.Timestamp = time.Unix(timestamp, 0) | 		allowance.Timestamp = time.Unix(timestamp, 0) | ||||||
| 		history = append(history, allowance) | 		history = append(history, allowance) | ||||||
| 	} | 	} | ||||||
| @@ -622,92 +493,3 @@ func (db *Db) GetHistory(userId int) ([]History, error) { | |||||||
| 	} | 	} | ||||||
| 	return history, nil | 	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() |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -10,50 +10,44 @@ type User struct { | |||||||
| type UserWithAllowance struct { | type UserWithAllowance struct { | ||||||
| 	ID        int    `json:"id"` | 	ID        int    `json:"id"` | ||||||
| 	Name      string `json:"name"` | 	Name      string `json:"name"` | ||||||
| 	Allowance float64 `json:"allowance"` | 	Allowance int    `json:"allowance"` | ||||||
| } | } | ||||||
|  |  | ||||||
| type History struct { | type History struct { | ||||||
| 	Allowance   float64   `json:"allowance"` | 	Allowance int       `json:"allowance"` | ||||||
| 	Timestamp time.Time `json:"timestamp"` | 	Timestamp time.Time `json:"timestamp"` | ||||||
| 	Description string    `json:"description"` |  | ||||||
| } | } | ||||||
|  |  | ||||||
| type PostHistory struct { | type PostHistory struct { | ||||||
| 	Allowance   float64 `json:"allowance"` | 	Allowance int `json:"allowance"` | ||||||
| 	Description string  `json:"description"` |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // Task represents a task in the system. | // Task represents a task in the system. | ||||||
| type Task struct { | type Task struct { | ||||||
| 	ID       int    `json:"id"` | 	ID       int    `json:"id"` | ||||||
| 	Name     string `json:"name"` | 	Name     string `json:"name"` | ||||||
| 	Reward   float64 `json:"reward"` | 	Reward   int    `json:"reward"` | ||||||
| 	Assigned *int    `json:"assigned"` | 	Assigned *int   `json:"assigned"` // Pointer to allow null | ||||||
| 	Schedule *string `json:"schedule"` |  | ||||||
| } | } | ||||||
|  |  | ||||||
| type Allowance struct { | type Allowance struct { | ||||||
| 	ID       int     `json:"id"` | 	ID       int     `json:"id"` | ||||||
| 	Name     string  `json:"name"` | 	Name     string  `json:"name"` | ||||||
| 	Target   float64 `json:"target"` | 	Target   int     `json:"target"` | ||||||
| 	Progress float64 `json:"progress"` | 	Progress int     `json:"progress"` | ||||||
| 	Weight   float64 `json:"weight"` | 	Weight   float64 `json:"weight"` | ||||||
| 	Colour   string  `json:"colour"` |  | ||||||
| } | } | ||||||
|  |  | ||||||
| type CreateAllowanceRequest struct { | type CreateAllowanceRequest struct { | ||||||
| 	Name   string  `json:"name"` | 	Name   string  `json:"name"` | ||||||
| 	Target float64 `json:"target"` | 	Target int     `json:"target"` | ||||||
| 	Weight float64 `json:"weight"` | 	Weight float64 `json:"weight"` | ||||||
| 	Colour string  `json:"colour"` |  | ||||||
| } | } | ||||||
|  |  | ||||||
| type UpdateAllowanceRequest struct { | type UpdateAllowanceRequest struct { | ||||||
| 	Name   string  `json:"name"` | 	Name   string  `json:"name"` | ||||||
| 	Target float64 `json:"target"` | 	Target int     `json:"target"` | ||||||
| 	Weight float64 `json:"weight"` | 	Weight float64 `json:"weight"` | ||||||
| 	Colour string  `json:"colour"` |  | ||||||
| } | } | ||||||
|  |  | ||||||
| type BulkUpdateAllowanceRequest struct { | type BulkUpdateAllowanceRequest struct { | ||||||
| @@ -67,16 +61,10 @@ type CreateGoalResponse struct { | |||||||
|  |  | ||||||
| type CreateTaskRequest struct { | type CreateTaskRequest struct { | ||||||
| 	Name     string `json:"name" binding:"required"` | 	Name     string `json:"name" binding:"required"` | ||||||
| 	Reward   float64 `json:"reward"` | 	Reward   int    `json:"reward"` | ||||||
| 	Assigned *int   `json:"assigned"` | 	Assigned *int   `json:"assigned"` | ||||||
| 	Schedule *string `json:"schedule"` |  | ||||||
| } | } | ||||||
|  |  | ||||||
| type CreateTaskResponse struct { | type CreateTaskResponse struct { | ||||||
| 	ID int `json:"id"` | 	ID int `json:"id"` | ||||||
| } | } | ||||||
|  |  | ||||||
| type AddAllowanceAmountRequest struct { |  | ||||||
| 	Amount      float64 `json:"amount"` |  | ||||||
| 	Description string  `json:"description"` |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -6,12 +6,11 @@ require ( | |||||||
| 	gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0 | 	gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0 | ||||||
| 	github.com/gavv/httpexpect/v2 v2.17.0 | 	github.com/gavv/httpexpect/v2 v2.17.0 | ||||||
| 	github.com/gin-contrib/cors v1.7.5 | 	github.com/gin-contrib/cors v1.7.5 | ||||||
| 	github.com/gin-gonic/gin v1.10.1 | 	github.com/gin-gonic/gin v1.10.0 | ||||||
| ) | ) | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect | 	github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect | ||||||
| 	github.com/adhocore/gronx v1.19.6 // indirect |  | ||||||
| 	github.com/ajg/form v1.5.1 // indirect | 	github.com/ajg/form v1.5.1 // indirect | ||||||
| 	github.com/andybalholm/brotli v1.1.1 // indirect | 	github.com/andybalholm/brotli v1.1.1 // indirect | ||||||
| 	github.com/bytedance/sonic v1.13.2 // indirect | 	github.com/bytedance/sonic v1.13.2 // indirect | ||||||
| @@ -50,7 +49,7 @@ require ( | |||||||
| 	github.com/sergi/go-diff v1.3.1 // indirect | 	github.com/sergi/go-diff v1.3.1 // indirect | ||||||
| 	github.com/stretchr/testify v1.10.0 // indirect | 	github.com/stretchr/testify v1.10.0 // indirect | ||||||
| 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | ||||||
| 	github.com/ugorji/go/codec v1.2.14 // indirect | 	github.com/ugorji/go/codec v1.2.12 // indirect | ||||||
| 	github.com/valyala/bytebufferpool v1.0.0 // indirect | 	github.com/valyala/bytebufferpool v1.0.0 // indirect | ||||||
| 	github.com/valyala/fasthttp v1.62.0 // indirect | 	github.com/valyala/fasthttp v1.62.0 // indirect | ||||||
| 	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect | 	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect | ||||||
| @@ -69,10 +68,10 @@ require ( | |||||||
| 	gopkg.in/fsnotify.v1 v1.4.7 // indirect | 	gopkg.in/fsnotify.v1 v1.4.7 // indirect | ||||||
| 	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect | 	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect | ||||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||||
| 	modernc.org/libc v1.65.8 // indirect | 	modernc.org/libc v1.65.7 // indirect | ||||||
| 	modernc.org/mathutil v1.7.1 // indirect | 	modernc.org/mathutil v1.7.1 // indirect | ||||||
| 	modernc.org/memory v1.11.0 // indirect | 	modernc.org/memory v1.11.0 // indirect | ||||||
| 	modernc.org/sqlite v1.37.1 // indirect | 	modernc.org/sqlite v1.37.0 // indirect | ||||||
| 	moul.io/http2curl/v2 v2.3.0 // indirect | 	moul.io/http2curl/v2 v2.3.0 // indirect | ||||||
| 	zombiezen.com/go/sqlite v1.4.2 // indirect | 	zombiezen.com/go/sqlite v1.4.0 // indirect | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -2,8 +2,6 @@ gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0 h1:aRItVfUj48fBmuec7rm/jY9KCfvHW | |||||||
| gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE= | gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE= | ||||||
| github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4= | github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4= | ||||||
| github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= | github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= | ||||||
| github.com/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 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= | ||||||
| github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= | github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= | ||||||
| github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= | ||||||
| @@ -36,8 +34,6 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w | |||||||
| github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= | ||||||
| github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= | ||||||
| github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= | ||||||
| github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= |  | ||||||
| github.com/gin-gonic/gin v1.10.1/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 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= | ||||||
| github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= | 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 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= | ||||||
| @@ -131,8 +127,6 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS | |||||||
| github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= | ||||||
| github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= | ||||||
| github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= | ||||||
| github.com/ugorji/go/codec v1.2.14 h1:yOQvXCBc3Ij46LRkRoh4Yd5qK6LVOgi0bYOXfb7ifjw= |  | ||||||
| github.com/ugorji/go/codec v1.2.14/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= |  | ||||||
| github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= | 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/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= | ||||||
| github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0= | github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0= | ||||||
| @@ -222,8 +216,6 @@ modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= | |||||||
| modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= | modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= | ||||||
| modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00= | modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00= | ||||||
| modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU= | modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU= | ||||||
| modernc.org/libc v1.65.8 h1:7PXRJai0TXZ8uNA3srsmYzmTyrLoHImV5QxHeni108Q= |  | ||||||
| modernc.org/libc v1.65.8/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU= |  | ||||||
| modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= | modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= | ||||||
| modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= | modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= | ||||||
| modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= | modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= | ||||||
| @@ -234,8 +226,6 @@ modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= | |||||||
| modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= | modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= | ||||||
| modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= | modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= | ||||||
| modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= | modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= | ||||||
| modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs= |  | ||||||
| modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g= |  | ||||||
| modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= | modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= | ||||||
| modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= | 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 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= | ||||||
| @@ -245,5 +235,3 @@ moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHc | |||||||
| nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= | 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 h1:N1s3RIljwtp4541Y8rM880qgGIgq3fTD2yks1xftnKU= | ||||||
| zombiezen.com/go/sqlite v1.4.0/go.mod h1:0w9F1DN9IZj9AcLS9YDKMboubCACkwYCGkzoy3eG5ik= | zombiezen.com/go/sqlite v1.4.0/go.mod h1:0w9F1DN9IZj9AcLS9YDKMboubCACkwYCGkzoy3eG5ik= | ||||||
| zombiezen.com/go/sqlite v1.4.2 h1:KZXLrBuJ7tKNEm+VJcApLMeQbhmAUOKA5VWS93DfFRo= |  | ||||||
| zombiezen.com/go/sqlite v1.4.2/go.mod h1:5Kd4taTAD4MkBzT25mQ9uaAlLjyR0rFhsR6iINO70jc= |  | ||||||
|   | |||||||
| @@ -4,9 +4,7 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"embed" | 	"embed" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" |  | ||||||
| 	"gitea.seeseepuff.be/seeseemelk/mysqlite" | 	"gitea.seeseepuff.be/seeseemelk/mysqlite" | ||||||
| 	"github.com/adhocore/gronx" |  | ||||||
| 	"log" | 	"log" | ||||||
| 	"net" | 	"net" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| @@ -45,11 +43,6 @@ type ServerConfig struct { | |||||||
| 	Started chan bool | 	Started chan bool | ||||||
| } | } | ||||||
|  |  | ||||||
| const DefaultDomain = "localhost:8080" |  | ||||||
|  |  | ||||||
| // The domain that the server is reachable at. |  | ||||||
| var domain = DefaultDomain |  | ||||||
|  |  | ||||||
| func getUsers(c *gin.Context) { | func getUsers(c *gin.Context) { | ||||||
| 	users, err := db.GetUsers() | 	users, err := db.GetUsers() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -375,56 +368,6 @@ func completeAllowance(c *gin.Context) { | |||||||
| 	c.IndentedJSON(http.StatusOK, gin.H{"message": "Allowance completed successfully"}) | 	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) { | func createTask(c *gin.Context) { | ||||||
| 	var taskRequest CreateTaskRequest | 	var taskRequest CreateTaskRequest | ||||||
| 	if err := c.ShouldBindJSON(&taskRequest); err != nil { | 	if err := c.ShouldBindJSON(&taskRequest); err != nil { | ||||||
| @@ -438,14 +381,6 @@ func createTask(c *gin.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if taskRequest.Schedule != nil { |  | ||||||
| 		valid := gronx.IsValid(*taskRequest.Schedule) |  | ||||||
| 		if !valid { |  | ||||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid cron schedule: %s", *taskRequest.Schedule)}) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// If assigned is not nil, check if user exists | 	// If assigned is not nil, check if user exists | ||||||
| 	if taskRequest.Assigned != nil { | 	if taskRequest.Assigned != nil { | ||||||
| 		exists, err := db.UserExists(*taskRequest.Assigned) | 		exists, err := db.UserExists(*taskRequest.Assigned) | ||||||
| @@ -523,11 +458,6 @@ func putTask(c *gin.Context) { | |||||||
| 		c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"}) | 		c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"}) | ||||||
| 		return | 		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) | 	err = db.UpdateTask(taskId, &taskRequest) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -609,11 +539,6 @@ func postHistory(c *gin.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if historyRequest.Description == "" { |  | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "Description cannot be empty"}) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	exists, err := db.UserExists(userId) | 	exists, err := db.UserExists(userId) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Printf(ErrCheckingUserExist, err) | 		log.Printf(ErrCheckingUserExist, err) | ||||||
| @@ -681,7 +606,6 @@ func start(ctx context.Context, config *ServerConfig) { | |||||||
| 	router.DELETE("/api/user/:userId/allowance/:allowanceId", deleteUserAllowance) | 	router.DELETE("/api/user/:userId/allowance/:allowanceId", deleteUserAllowance) | ||||||
| 	router.PUT("/api/user/:userId/allowance/:allowanceId", putUserAllowance) | 	router.PUT("/api/user/:userId/allowance/:allowanceId", putUserAllowance) | ||||||
| 	router.POST("/api/user/:userId/allowance/:allowanceId/complete", completeAllowance) | 	router.POST("/api/user/:userId/allowance/:allowanceId/complete", completeAllowance) | ||||||
| 	router.POST("/api/user/:userId/allowance/:allowanceId/add", addToAllowance) |  | ||||||
| 	router.POST("/api/tasks", createTask) | 	router.POST("/api/tasks", createTask) | ||||||
| 	router.GET("/api/tasks", getTasks) | 	router.GET("/api/tasks", getTasks) | ||||||
| 	router.GET("/api/task/:taskId", getTask) | 	router.GET("/api/task/:taskId", getTask) | ||||||
| @@ -726,10 +650,5 @@ func main() { | |||||||
| 		config.Datasource = "allowance_planner.db3" | 		config.Datasource = "allowance_planner.db3" | ||||||
| 		log.Printf("Warning: No DB_PATH set, using default of %s", config.Datasource) | 		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) | 	start(context.Background(), &config) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ create table users | |||||||
| ( | ( | ||||||
|     id   integer primary key, |     id   integer primary key, | ||||||
|     name text not null, |     name text not null, | ||||||
|     weight real not null default 10.0, |     weight real not null default 0.0, | ||||||
|     balance integer not null default 0 |     balance integer not null default 0 | ||||||
| ) strict; | ) 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 ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"log" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| ) | ) | ||||||
| @@ -27,22 +26,11 @@ func loadWebEndpoints(router *gin.Engine) { | |||||||
| 	router.GET("/completeAllowance", renderCompleteAllowance) | 	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) { | func renderLogin(c *gin.Context) { | ||||||
| 	if c.Query("user") != "" { | 	if c.Query("user") != "" { | ||||||
| 		log.Println("Set cookie for user:", c.Query("user")) | 		c.SetCookie("user", c.Query("user"), 3600, "/", "localhost", false, true) | ||||||
| 		c.SetCookie("user", c.Query("user"), 3600, "", "", false, true) |  | ||||||
| 	} | 	} | ||||||
| 	redirectToPage(c, "/") | 	c.Redirect(http.StatusFound, "/") | ||||||
| } | } | ||||||
|  |  | ||||||
| func renderIndex(c *gin.Context) { | func renderIndex(c *gin.Context) { | ||||||
| @@ -61,7 +49,7 @@ func renderCreateTask(c *gin.Context) { | |||||||
|  |  | ||||||
| 	name := c.PostForm("name") | 	name := c.PostForm("name") | ||||||
| 	rewardStr := c.PostForm("reward") | 	rewardStr := c.PostForm("reward") | ||||||
| 	reward, err := strconv.ParseFloat(rewardStr, 64) | 	reward, err := strconv.Atoi(rewardStr) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		renderError(c, http.StatusBadRequest, err) | 		renderError(c, http.StatusBadRequest, err) | ||||||
| 		return | 		return | ||||||
| @@ -71,24 +59,16 @@ func renderCreateTask(c *gin.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	request := &CreateTaskRequest{ | 	_, err = db.CreateTask(&CreateTaskRequest{ | ||||||
| 		Name:   name, | 		Name:   name, | ||||||
| 		Reward: reward, | 		Reward: reward, | ||||||
| 	} | 	}) | ||||||
|  |  | ||||||
| 	schedule := c.PostForm("schedule") |  | ||||||
| 	if schedule != "" { |  | ||||||
| 		request.Schedule = &schedule |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	_, err = db.CreateTask(request) |  | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		renderError(c, http.StatusInternalServerError, err) | 		renderError(c, http.StatusInternalServerError, err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	redirectToPageStatus(c, "/", http.StatusFound) | 	c.Redirect(http.StatusFound, "/") | ||||||
| } | } | ||||||
|  |  | ||||||
| func renderCompleteTask(c *gin.Context) { | func renderCompleteTask(c *gin.Context) { | ||||||
| @@ -105,7 +85,7 @@ func renderCompleteTask(c *gin.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	redirectToPageStatus(c, "/", http.StatusFound) | 	c.Redirect(http.StatusFound, "/") | ||||||
| } | } | ||||||
|  |  | ||||||
| func renderCreateAllowance(c *gin.Context) { | func renderCreateAllowance(c *gin.Context) { | ||||||
| @@ -116,7 +96,7 @@ func renderCreateAllowance(c *gin.Context) { | |||||||
|  |  | ||||||
| 	name := c.PostForm("name") | 	name := c.PostForm("name") | ||||||
| 	targetStr := c.PostForm("target") | 	targetStr := c.PostForm("target") | ||||||
| 	target, err := strconv.ParseFloat(targetStr, 64) | 	target, err := strconv.Atoi(targetStr) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		renderError(c, http.StatusBadRequest, err) | 		renderError(c, http.StatusBadRequest, err) | ||||||
| 		return | 		return | ||||||
| @@ -142,7 +122,7 @@ func renderCreateAllowance(c *gin.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	redirectToPageStatus(c, "/", http.StatusFound) | 	c.Redirect(http.StatusFound, "/") | ||||||
| } | } | ||||||
|  |  | ||||||
| func renderCompleteAllowance(c *gin.Context) { | func renderCompleteAllowance(c *gin.Context) { | ||||||
| @@ -164,12 +144,11 @@ func renderCompleteAllowance(c *gin.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	redirectToPageStatus(c, "/", http.StatusFound) | 	c.Redirect(http.StatusFound, "/") | ||||||
| } | } | ||||||
|  |  | ||||||
| func getCurrentUser(c *gin.Context) *int { | func getCurrentUser(c *gin.Context) *int { | ||||||
| 	currentUserStr, err := c.Cookie("user") | 	currentUserStr, err := c.Cookie("user") | ||||||
| 	log.Println("Cookie string:", currentUserStr) |  | ||||||
| 	if errors.Is(err, http.ErrNoCookie) { | 	if errors.Is(err, http.ErrNoCookie) { | ||||||
| 		renderNoUser(c) | 		renderNoUser(c) | ||||||
| 		return nil | 		return nil | ||||||
| @@ -193,7 +172,7 @@ func getCurrentUser(c *gin.Context) *int { | |||||||
|  |  | ||||||
| func unsetUserCookie(c *gin.Context) { | func unsetUserCookie(c *gin.Context) { | ||||||
| 	c.SetCookie("user", "", -1, "/", "localhost", false, true) | 	c.SetCookie("user", "", -1, "/", "localhost", false, true) | ||||||
| 	redirectToPageStatus(c, "/", http.StatusFound) | 	c.Redirect(http.StatusFound, "/") | ||||||
| } | } | ||||||
|  |  | ||||||
| func renderNoUser(c *gin.Context) { | func renderNoUser(c *gin.Context) { | ||||||
|   | |||||||
| @@ -3,11 +3,9 @@ | |||||||
| <head> | <head> | ||||||
| 	<title>Allowance Planner 2000</title> | 	<title>Allowance Planner 2000</title> | ||||||
| 	<style> | 	<style> | ||||||
| 		<!-- |  | ||||||
| 		tr:hover { | 		tr:hover { | ||||||
| 			background-color: #f0f0f0; | 			background-color: #f0f0f0; | ||||||
| 		} | 		} | ||||||
| 		--> |  | ||||||
| 	</style> | 	</style> | ||||||
| </head> | </head> | ||||||
| <body> | <body> | ||||||
| @@ -29,7 +27,7 @@ | |||||||
| 	{{if ne .CurrentUser 0}} | 	{{if ne .CurrentUser 0}} | ||||||
| 		<h2>Allowances</h2> | 		<h2>Allowances</h2> | ||||||
| 		<form action="/createAllowance" method="post"> | 		<form action="/createAllowance" method="post"> | ||||||
| 			<table border=1> | 			<table border="1"> | ||||||
| 				<thead> | 				<thead> | ||||||
| 				<tr> | 				<tr> | ||||||
| 					<th>Name</th> | 					<th>Name</th> | ||||||
| @@ -45,7 +43,7 @@ | |||||||
| 						<td></td> | 						<td></td> | ||||||
| 						<td><label><input type="number" name="target" placeholder="Target"></label></td> | 						<td><label><input type="number" name="target" placeholder="Target"></label></td> | ||||||
| 						<td><label><input type="number" name="weight" placeholder="Weight"></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> | 					</tr> | ||||||
| 				{{range .Allowances}} | 				{{range .Allowances}} | ||||||
| 					{{if eq .ID 0}} | 					{{if eq .ID 0}} | ||||||
| @@ -81,7 +79,6 @@ | |||||||
| 					<th>Name</th> | 					<th>Name</th> | ||||||
| 					<th>Assigned</th> | 					<th>Assigned</th> | ||||||
| 					<th>Reward</th> | 					<th>Reward</th> | ||||||
| 					<th>Schedule</th> |  | ||||||
| 					<th>Actions</th> | 					<th>Actions</th> | ||||||
| 				</tr> | 				</tr> | ||||||
| 				</thead> | 				</thead> | ||||||
| @@ -97,7 +94,6 @@ | |||||||
| 							{{end}} | 							{{end}} | ||||||
| 						</td> | 						</td> | ||||||
| 						<td>{{.Reward}}</td> | 						<td>{{.Reward}}</td> | ||||||
| 						<td>{{.Schedule}}</td> |  | ||||||
| 						<td> | 						<td> | ||||||
| 							<a href="/completeTask?task={{.ID}}">Mark as completed</a> | 							<a href="/completeTask?task={{.ID}}">Mark as completed</a> | ||||||
| 						</td> | 						</td> | ||||||
| @@ -107,8 +103,7 @@ | |||||||
| 							<td><label><input type="text" name="name" placeholder="Name"></label></td> | 							<td><label><input type="text" name="name" placeholder="Name"></label></td> | ||||||
| 							<td></td> | 							<td></td> | ||||||
| 							<td><label><input type="number" name="reward" placeholder="Reward"></label></td> | 							<td><label><input type="number" name="reward" placeholder="Reward"></label></td> | ||||||
| 							<td><label><input type="text" name="schedule" placeholder="Schedule"></label></td> | 							<td><button>Create</button></td> | ||||||
| 							<td><input type="submit" value="Create"></td> |  | ||||||
| 						</tr> | 						</tr> | ||||||
| 				</tbody> | 				</tbody> | ||||||
| 			</table> | 			</table> | ||||||
|   | |||||||
| @@ -422,10 +422,7 @@ components: | |||||||
|           description: The task name |           description: The task name | ||||||
|         reward: |         reward: | ||||||
|           type: integer |           type: integer | ||||||
|           description: The task reward |           description: The task reward, in cents | ||||||
|         schedule: |  | ||||||
|           type: string |  | ||||||
|           description: The schedule of the task, in cron format |  | ||||||
|         assigned: |         assigned: | ||||||
|           type: integer |           type: integer | ||||||
|           description: The user ID of the user assigned to the task |           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 = { | const config: CapacitorConfig = { | ||||||
|   appId: 'io.ionic.starter', |   appId: 'io.ionic.starter', | ||||||
|   appName: 'Allowance Planner V2', |   appName: 'allowance-planner-v2', | ||||||
|   webDir: 'www' |   webDir: 'www' | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										3478
									
								
								frontend/allowance-planner-v2/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						| @@ -23,7 +23,6 @@ | |||||||
|     "@angular/platform-browser": "^19.0.0", |     "@angular/platform-browser": "^19.0.0", | ||||||
|     "@angular/platform-browser-dynamic": "^19.0.0", |     "@angular/platform-browser-dynamic": "^19.0.0", | ||||||
|     "@angular/router": "^19.0.0", |     "@angular/router": "^19.0.0", | ||||||
|     "@capacitor/android": "7.2.0", |  | ||||||
|     "@capacitor/app": "7.0.1", |     "@capacitor/app": "7.0.1", | ||||||
|     "@capacitor/core": "7.2.0", |     "@capacitor/core": "7.2.0", | ||||||
|     "@capacitor/haptics": "7.0.1", |     "@capacitor/haptics": "7.0.1", | ||||||
| @@ -47,7 +46,6 @@ | |||||||
|     "@angular/cli": "^19.0.0", |     "@angular/cli": "^19.0.0", | ||||||
|     "@angular/compiler-cli": "^19.0.0", |     "@angular/compiler-cli": "^19.0.0", | ||||||
|     "@angular/language-service": "^19.0.0", |     "@angular/language-service": "^19.0.0", | ||||||
|     "@capacitor/assets": "^3.0.5", |  | ||||||
|     "@capacitor/cli": "7.2.0", |     "@capacitor/cli": "7.2.0", | ||||||
|     "@ionic/angular-toolkit": "^12.0.0", |     "@ionic/angular-toolkit": "^12.0.0", | ||||||
|     "@types/jasmine": "~5.1.0", |     "@types/jasmine": "~5.1.0", | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ const routes: Routes = [ | |||||||
|     path: '', |     path: '', | ||||||
|     loadChildren: () => import('./pages/tabs/tabs.module').then(m => m.TabsPageModule) |     loadChildren: () => import('./pages/tabs/tabs.module').then(m => m.TabsPageModule) | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
| ]; | ]; | ||||||
| @NgModule({ | @NgModule({ | ||||||
|   imports: [ |   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(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||