2 Commits

276 changed files with 190 additions and 36350 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -1,16 +1,2 @@
# Allowance Planner 2000 # Allowance Planner 2000
An improved Allowance Planner app. An improved Allowance Planner app.
## Running backend
In order to run the backend, go to the `backend` directory and run:
```bash
$ go run .
```
## Running frontend
In order to run the frontend, go to the `allowance-planner-v2` directory in the `frontend` directory and run:
```bash
$ ionic serve
```

1
backend/.gitignore vendored
View File

@@ -1,3 +1,2 @@
*.db3 *.db3
*.db3-* *.db3-*
/allowance_planner

View File

@@ -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"]

View File

@@ -9,7 +9,7 @@ import (
) )
const ( const (
TestHistoryName = "Test History" TestGoalName = "Test Goal"
) )
func startServer(t *testing.T) *httpexpect.Expect { func startServer(t *testing.T) *httpexpect.Expect {
@@ -49,58 +49,56 @@ func TestGetUserBadId(t *testing.T) {
e.GET("/user/bad-id").Expect().Status(400) e.GET("/user/bad-id").Expect().Status(400)
} }
func TestGetUserAllowanceWhenNoAllowancePresent(t *testing.T) { func TestGetUserGoalsWhenNoGoalsPresent(t *testing.T) {
e := startServer(t) e := startServer(t)
result := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array() result := e.GET("/user/1/goals").Expect().Status(200).JSON().Array()
result.Length().IsEqual(1) result.Length().IsEqual(0)
item := result.Value(0).Object()
item.Value("id").IsEqual(0)
} }
func TestGetUserAllowance(t *testing.T) { func TestGetUserGoals(t *testing.T) {
e := startServer(t) e := startServer(t)
// Create a new allowance // Create a new goal
requestBody := map[string]interface{}{ requestBody := map[string]interface{}{
"name": TestHistoryName, "name": TestGoalName,
"target": 5000, "target": 5000,
"weight": 10, "weight": 10,
} }
e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201) e.POST("/user/1/goals").WithJSON(requestBody).Expect().Status(201)
// Validate allowance // Validate goal
result := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array() result := e.GET("/user/1/goals").Expect().Status(200).JSON().Array()
result.Length().IsEqual(2) result.Length().IsEqual(1)
item := result.Value(1).Object() item := result.Value(0).Object()
item.Value("id").IsEqual(1) item.Value("id").IsEqual(1)
item.Value("name").IsEqual(TestHistoryName) item.Value("name").IsEqual(TestGoalName)
item.Value("target").IsEqual(5000) item.Value("target").IsEqual(5000)
item.Value("weight").IsEqual(10) item.Value("weight").IsEqual(10)
item.Value("progress").IsEqual(0) item.Value("progress").IsEqual(0)
item.NotContainsKey("user_id") item.NotContainsKey("user_id")
} }
func TestGetUserAllowanceNoUser(t *testing.T) { func TestGetUserGoalsNoUser(t *testing.T) {
e := startServer(t) e := startServer(t)
e.GET("/user/999/allowance").Expect().Status(404) e.GET("/user/999/goals").Expect().Status(404)
} }
func TestGetUserAllowanceBadId(t *testing.T) { func TestGetUserGoalsBadId(t *testing.T) {
e := startServer(t) e := startServer(t)
e.GET("/user/bad-id/allowance").Expect().Status(400) e.GET("/user/bad-id/goals").Expect().Status(400)
} }
func TestCreateUserAllowance(t *testing.T) { func TestCreateUserGoal(t *testing.T) {
e := startServer(t) e := startServer(t)
// Create a new allowance // Create a new goal
requestBody := map[string]interface{}{ requestBody := map[string]interface{}{
"name": TestHistoryName, "name": TestGoalName,
"target": 5000, "target": 5000,
"weight": 10, "weight": 10,
} }
response := e.POST("/user/1/allowance"). response := e.POST("/user/1/goals").
WithJSON(requestBody). WithJSON(requestBody).
Expect(). Expect().
Status(201). Status(201).
@@ -108,40 +106,40 @@ func TestCreateUserAllowance(t *testing.T) {
// Verify the response has an ID // Verify the response has an ID
response.ContainsKey("id") response.ContainsKey("id")
allowanceId := response.Value("id").Number().Raw() goalId := response.Value("id").Number().Raw()
// Verify the allowance exists in the list of allowances // Verify the goal exists in the list of goals
allowances := e.GET("/user/1/allowance"). goals := e.GET("/user/1/goals").
Expect(). Expect().
Status(200). Status(200).
JSON().Array() JSON().Array()
allowances.Length().IsEqual(2) goals.Length().IsEqual(1)
allowance := allowances.Value(1).Object() goal := goals.Value(0).Object()
allowance.Value("id").IsEqual(allowanceId) goal.Value("id").IsEqual(goalId)
allowance.Value("name").IsEqual(TestHistoryName) goal.Value("name").IsEqual(TestGoalName)
allowance.Value("target").IsEqual(5000) goal.Value("target").IsEqual(5000)
allowance.Value("weight").IsEqual(10) goal.Value("weight").IsEqual(10)
allowance.Value("progress").IsEqual(0) goal.Value("progress").IsEqual(0)
} }
func TestCreateUserAllowanceNoUser(t *testing.T) { func TestCreateUserGoalNoUser(t *testing.T) {
e := startServer(t) e := startServer(t)
requestBody := map[string]interface{}{ requestBody := map[string]interface{}{
"name": TestHistoryName, "name": TestGoalName,
"target": 5000, "target": 5000,
"weight": 10, "weight": 10,
} }
e.POST("/user/999/allowance"). e.POST("/user/999/goals").
WithJSON(requestBody). WithJSON(requestBody).
Expect(). Expect().
Status(404) Status(404)
} }
func TestCreateUserAllowanceInvalidInput(t *testing.T) { func TestCreateUserGoalInvalidInput(t *testing.T) {
e := startServer(t) e := startServer(t)
// Test with empty name // Test with empty name
@@ -151,7 +149,7 @@ func TestCreateUserAllowanceInvalidInput(t *testing.T) {
"weight": 10, "weight": 10,
} }
e.POST("/user/1/allowance"). e.POST("/user/1/goals").
WithJSON(requestBody). WithJSON(requestBody).
Expect(). Expect().
Status(400) Status(400)
@@ -161,81 +159,76 @@ func TestCreateUserAllowanceInvalidInput(t *testing.T) {
"target": 5000, "target": 5000,
} }
e.POST("/user/1/allowance"). e.POST("/user/1/goals").
WithJSON(invalidRequest). WithJSON(invalidRequest).
Expect(). Expect().
Status(400) Status(400)
} }
func TestCreateUserAllowanceBadId(t *testing.T) { func TestCreateUserGoalBadId(t *testing.T) {
e := startServer(t) e := startServer(t)
requestBody := map[string]interface{}{ requestBody := map[string]interface{}{
"name": TestHistoryName, "name": TestGoalName,
"target": 5000, "target": 5000,
"weight": 10, "weight": 10,
} }
e.POST("/user/bad-id/allowance"). e.POST("/user/bad-id/goals").
WithJSON(requestBody). WithJSON(requestBody).
Expect(). Expect().
Status(400) Status(400)
} }
func TestDeleteUserAllowance(t *testing.T) { func TestDeleteUserGoal(t *testing.T) {
e := startServer(t) e := startServer(t)
// Create a new allowance to delete // Create a new goal to delete
createRequest := map[string]interface{}{ createRequest := map[string]interface{}{
"name": TestHistoryName, "name": TestGoalName,
"target": 1000, "target": 1000,
"weight": 5, "weight": 5,
} }
response := e.POST("/user/1/allowance"). response := e.POST("/user/1/goals").
WithJSON(createRequest). WithJSON(createRequest).
Expect(). Expect().
Status(201). Status(201).
JSON().Object() JSON().Object()
allowanceId := response.Value("id").Number().Raw() goalId := response.Value("id").Number().Raw()
// Delete the allowance // Delete the goal
e.DELETE("/user/1/allowance/" + strconv.Itoa(int(allowanceId))). e.DELETE("/user/1/goal/" + strconv.Itoa(int(goalId))).
Expect(). Expect().
Status(200). Status(200).
JSON().Object().Value("message").IsEqual("History deleted successfully") JSON().Object().Value("message").IsEqual("Goal deleted successfully")
// Verify the allowance no longer exists // Verify the goal no longer exists
allowances := e.GET("/user/1/allowance"). goals := e.GET("/user/1/goals").
Expect(). Expect().
Status(200). Status(200).
JSON().Array() JSON().Array()
allowances.Length().IsEqual(1) goals.Length().IsEqual(0)
} }
func TestDeleteUserRestAllowance(t *testing.T) { func TestDeleteUserGoalNotFound(t *testing.T) {
e := startServer(t)
e.DELETE("/user/1/allowance/0").Expect().Status(400)
}
func TestDeleteUserAllowanceNotFound(t *testing.T) {
e := startServer(t) e := startServer(t)
// Attempt to delete a non-existent allowance // Attempt to delete a non-existent goal
e.DELETE("/user/1/allowance/999"). e.DELETE("/user/1/goal/999").
Expect(). Expect().
Status(404). Status(404).
JSON().Object().Value("error").IsEqual("History not found") JSON().Object().Value("error").IsEqual("Goal not found")
} }
func TestDeleteUserAllowanceInvalidId(t *testing.T) { func TestDeleteUserGoalInvalidId(t *testing.T) {
e := startServer(t) e := startServer(t)
// Attempt to delete an allowance with an invalid ID // Attempt to delete a goal with an invalid ID
e.DELETE("/user/1/allowance/invalid-id"). e.DELETE("/user/1/goal/invalid-id").
Expect(). Expect().
Status(400). Status(400).
JSON().Object().Value("error").IsEqual("Invalid allowance ID") JSON().Object().Value("error").IsEqual("Invalid goal ID")
} }
func TestCreateTask(t *testing.T) { func TestCreateTask(t *testing.T) {
@@ -255,16 +248,7 @@ func TestCreateTask(t *testing.T) {
// Verify the response has an ID // Verify the response has an ID
response.ContainsKey("id") response.ContainsKey("id")
response.Value("id").Number().IsEqual(1) taskId := response.Value("id").Number().Raw()
e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)
// Get task
result := e.GET("/task/1").Expect().Status(200).JSON().Object()
result.Value("id").IsEqual(1)
result.Value("name").IsEqual("Test Task")
result.Value("reward").IsEqual(100)
result.Value("assigned").IsNull()
// Create a new task with assigned user // Create a new task with assigned user
assignedUserId := 1 assignedUserId := 1
@@ -281,37 +265,7 @@ func TestCreateTask(t *testing.T) {
JSON().Object() JSON().Object()
responseWithUser.ContainsKey("id") responseWithUser.ContainsKey("id")
responseWithUser.Value("id").Number().IsEqual(2) responseWithUser.Value("id").Number().NotEqual(taskId) // Ensure different ID
}
func TestDeleteTask(t *testing.T) {
e := startServer(t)
// Create a new task without assigned user
requestBody := map[string]interface{}{
"name": "Test Task",
"reward": 100,
}
response := e.POST("/tasks").
WithJSON(requestBody).
Expect().
Status(201). // Expect Created status
JSON().Object()
// Verify the response has an ID
response.ContainsKey("id")
taskId := response.Value("id").Number().Raw()
// Delete the task
e.DELETE("/task/" + strconv.Itoa(int(taskId))).Expect().Status(200)
// Verify the task no longer exists
e.GET("/task/" + strconv.Itoa(int(taskId))).Expect().Status(404)
}
func TestDeleteTaskNotFound(t *testing.T) {
e := startServer(t)
e.DELETE("/task/1").Expect().Status(404)
} }
func TestCreateTaskNoName(t *testing.T) { func TestCreateTaskNoName(t *testing.T) {
@@ -361,15 +315,15 @@ func TestGetTaskWhenNoTasks(t *testing.T) {
result.Length().IsEqual(0) result.Length().IsEqual(0)
} }
func createTestTaskWithAmount(e *httpexpect.Expect, amount int) int { func createTestTask(e *httpexpect.Expect) {
requestBody := map[string]interface{}{ requestBody := map[string]interface{}{
"name": "Test Task", "name": "Test Task",
"reward": amount, "reward": 100,
} }
return int(e.POST("/tasks").WithJSON(requestBody).Expect().Status(201).JSON().Object().Value("id").Number().Raw()) e.POST("/tasks").WithJSON(requestBody).Expect().Status(201)
} }
func TestGetTasksWhenTasks(t *testing.T) { func TestGetTaskSWhenTasks(t *testing.T) {
e := startServer(t) e := startServer(t)
createTestTask(e) createTestTask(e)
@@ -434,483 +388,35 @@ 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/allowance").WithJSON(PostAllowance{Allowance: 100}).Expect().Status(200)
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 20, Description: "Lolol"}).Expect().Status(200) e.POST("/user/1/allowance").WithJSON(PostAllowance{Allowance: 20}).Expect().Status(200)
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: -10, Description: "Subtracting"}).Expect().Status(200) e.POST("/user/1/allowance").WithJSON(PostAllowance{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/allowance").WithJSON(PostAllowance{Allowance: 100}).Expect().
Status(404) Status(404)
} }
func TestPostHistoryInvalidDescription(t *testing.T) { func TestGetAllowance(t *testing.T) {
e := startServer(t) e := startServer(t)
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 100}).Expect(). e.POST("/user/1/allowance").WithJSON(PostAllowance{Allowance: 100}).Expect().Status(200)
Status(400) e.POST("/user/1/allowance").WithJSON(PostAllowance{Allowance: 20}).Expect().Status(200)
} e.POST("/user/1/allowance").WithJSON(PostAllowance{Allowance: -10}).Expect().Status(200)
func TestGetHistory(t *testing.T) { response := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
e := startServer(t)
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 100, Description: "Add 100"}).Expect().Status(200)
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 20, Description: "Add 20"}).Expect().Status(200)
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: -10, Description: "Subtract 10"}).Expect().Status(200)
response := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
response.Length().IsEqual(3) response.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").Number().NotEqual(0).InDelta(float64(time.Now().Unix()), 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) {
e := startServer(t)
// Create a new allowance
requestBody := map[string]interface{}{
"name": TestHistoryName,
"target": 5000,
"weight": 10,
"colour": "#FF5733",
}
resp := e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201).JSON().Object()
allowanceId := int(resp.Value("id").Number().Raw())
// Retrieve the created allowance by ID
result := e.GET("/user/1/allowance/" + strconv.Itoa(allowanceId)).Expect().Status(200).JSON().Object()
result.Value("id").IsEqual(allowanceId)
result.Value("name").IsEqual(TestHistoryName)
result.Value("target").IsEqual(5000)
result.Value("weight").IsEqual(10)
result.Value("progress").IsEqual(0)
result.Value("colour").IsEqual("#FF5733")
resultArray := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
resultArray.Length().IsEqual(2)
result = resultArray.Value(1).Object()
result.Value("id").IsEqual(allowanceId)
result.Value("name").IsEqual(TestHistoryName)
result.Value("target").IsEqual(5000)
result.Value("weight").IsEqual(10)
result.Value("progress").IsEqual(0)
result.Value("colour").IsEqual("#FF5733")
}
func TestGetUserByAllowanceIdInvalidAllowance(t *testing.T) {
e := startServer(t)
e.GET("/user/1/allowance/9999").Expect().Status(404)
}
func TestGetUserByAllowanceByIdInvalidUserId(t *testing.T) {
e := startServer(t)
e.GET("/user/999/allowance/1").Expect().Status(404)
}
func TestGetUserByAllowanceByIdBadUserId(t *testing.T) {
e := startServer(t)
e.GET("/user/bad/allowance/1").Expect().Status(400)
}
func TestGetUserByAllowanceByIdBadAllowanceId(t *testing.T) {
e := startServer(t)
e.GET("/user/1/allowance/bad").Expect().Status(400)
}
func TestPutAllowanceById(t *testing.T) {
e := startServer(t)
// Create a new allowance
requestBody := map[string]interface{}{
"name": TestHistoryName,
"target": 5000,
"weight": 10,
"colour": "#FF5733",
}
resp := e.POST("/user/1/allowance").WithJSON(requestBody).Expect().Status(201).JSON().Object()
allowanceId := int(resp.Value("id").Number().Raw())
// Update the allowance
updateRequest := map[string]interface{}{
"name": "Updated Allowance",
"target": 6000,
"weight": 15,
"colour": "#3357FF",
}
e.PUT("/user/1/allowance/" + strconv.Itoa(allowanceId)).WithJSON(updateRequest).Expect().Status(200)
// Verify the allowance is updated
result := e.GET("/user/1/allowance/" + strconv.Itoa(allowanceId)).Expect().Status(200).JSON().Object()
result.Value("id").IsEqual(allowanceId)
result.Value("name").IsEqual("Updated Allowance")
result.Value("target").IsEqual(6000)
result.Value("weight").IsEqual(15)
result.Value("colour").IsEqual("#3357FF")
}
func TestCompleteTask(t *testing.T) {
e := startServer(t)
taskId := createTestTaskWithAmount(e, 101)
e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)
// Update rest allowance
e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{
Weight: 25,
}).Expect().Status(200)
// Create two allowance goals
e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
Name: "Test Allowance 1",
Target: 100,
Weight: 50,
}).Expect().Status(201)
e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
Name: "Test Allowance 1",
Target: 10,
Weight: 25,
}).Expect().Status(201)
// Complete the task
e.POST("/task/" + strconv.Itoa(taskId) + "/complete").Expect().Status(200)
// Verify the task is marked as completed
e.GET("/task/" + strconv.Itoa(taskId)).Expect().Status(404)
// Verify the allowances are updated for user 1
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Length().IsEqual(3)
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
allowances.Value(0).Object().Value("progress").Number().InDelta(30.34, 0.01)
allowances.Value(1).Object().Value("id").Number().IsEqual(1)
allowances.Value(1).Object().Value("progress").Number().InDelta(60.66, 0.01)
allowances.Value(2).Object().Value("id").Number().IsEqual(2)
allowances.Value(2).Object().Value("progress").Number().IsEqual(10)
// And also for user 2
allowances = e.GET("/user/2/allowance").Expect().Status(200).JSON().Array()
allowances.Length().IsEqual(1)
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
allowances.Value(0).Object().Value("progress").Number().IsEqual(101)
for userId := 1; userId <= 2; userId++ {
userIdStr := strconv.Itoa(userId)
// Ensure the history got updated
history := e.GET("/user/" + userIdStr + "/history").Expect().Status(200).JSON().Array()
history.Length().IsEqual(1)
history.Value(0).Object().Value("allowance").Number().IsEqual(101)
history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
}
}
func TestCompleteTaskWithNoWeights(t *testing.T) {
e := startServer(t)
taskId := createTestTaskWithAmount(e, 101)
e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)
// Ensure main allowance has no weight
e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{
Weight: 0,
}).Expect().Status(200)
// Complete the task
e.POST("/task/" + strconv.Itoa(taskId) + "/complete").Expect().Status(200)
// Verify the task is marked as completed
e.GET("/task/" + strconv.Itoa(taskId)).Expect().Status(404)
// Verify the allowances are updated for user 1
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Length().IsEqual(1)
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
allowances.Value(0).Object().Value("progress").Number().InDelta(101.00, 0.01)
// And also for user 2
allowances = e.GET("/user/2/allowance").Expect().Status(200).JSON().Array()
allowances.Length().IsEqual(1)
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
allowances.Value(0).Object().Value("progress").Number().InDelta(101.00, 0.01)
}
func TestCompleteTaskAllowanceWeightsSumTo0(t *testing.T) {
e := startServer(t)
taskId := createTestTaskWithAmount(e, 101)
e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)
// Update rest allowance
e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{
Weight: 0,
}).Expect().Status(200)
// Create an allowance goal
createTestAllowance(e, "Test Allowance 1", 1000, 0)
// Complete the task
e.POST("/task/" + strconv.Itoa(taskId) + "/complete").Expect().Status(200)
// Verify the task is marked as completed
e.GET("/task/" + strconv.Itoa(taskId)).Expect().Status(404)
// Verify the allowances are updated for user 1
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Length().IsEqual(2)
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
allowances.Value(0).Object().Value("progress").Number().IsEqual(101)
allowances.Value(1).Object().Value("id").Number().IsEqual(1)
allowances.Value(1).Object().Value("progress").Number().IsEqual(0)
}
func TestCompleteTaskInvalidId(t *testing.T) {
e := startServer(t)
e.POST("/task/999/complete").Expect().Status(404)
}
func TestCompleteAllowance(t *testing.T) {
e := startServer(t)
createTestTaskWithAmount(e, 100)
createTestAllowance(e, "Test Allowance 1", 100, 50)
// Update base allowance
e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{
Weight: 0,
}).Expect().Status(200)
// Complete the task
e.POST("/task/1/complete").Expect().Status(200)
// Complete allowance goal
e.POST("/user/1/allowance/1/complete").Expect().Status(200)
// Verify the allowance no longer exists
e.GET("/user/1/allowance/1").Expect().Status(404)
// Verify history is updated
history := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
history.Length().IsEqual(2)
history.Value(0).Object().Length().IsEqual(3)
history.Value(0).Object().Value("allowance").Number().IsEqual(100)
history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
history.Value(0).Object().Value("description").String().IsEqual("Task completed: Test Task")
history.Value(1).Object().Length().IsEqual(3)
history.Value(1).Object().Value("allowance").Number().IsEqual(-100)
history.Value(1).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
history.Value(1).Object().Value("description").String().IsEqual("Allowance completed: Test Allowance 1")
}
func TestCompleteAllowanceInvalidUserId(t *testing.T) {
e := startServer(t)
e.POST("/user/999/allowance/1/complete").Expect().Status(404)
}
func TestCompleteAllowanceInvalidAllowanceId(t *testing.T) {
e := startServer(t)
e.POST("/user/1/allowance/999/complete").Expect().Status(404)
}
func TestPutBulkAllowance(t *testing.T) {
e := startServer(t)
createTestAllowance(e, "Test Allowance 1", 1000, 1)
createTestAllowance(e, "Test Allowance 2", 1000, 2)
// Bulk edit
request := []map[string]interface{}{
{
"id": 1,
"weight": 5,
},
{
"id": 0,
"weight": 99,
},
{
"id": 2,
"weight": 10,
},
}
e.PUT("/user/1/allowance").WithJSON(request).Expect().Status(200)
// Verify the allowances are updated
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Length().IsEqual(3)
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
allowances.Value(0).Object().Value("weight").Number().IsEqual(99)
allowances.Value(1).Object().Value("id").Number().IsEqual(1)
allowances.Value(1).Object().Value("weight").Number().IsEqual(5)
allowances.Value(2).Object().Value("id").Number().IsEqual(2)
allowances.Value(2).Object().Value("weight").Number().IsEqual(10)
}
func TestAddAllowanceSimple(t *testing.T) {
e := startServer(t)
createTestAllowance(e, "Test Allowance 1", 1000, 1)
request := map[string]interface{}{
"amount": 10,
"description": "Added to allowance 1",
}
e.POST("/user/1/allowance/1/add").WithJSON(request).Expect().Status(200)
// Verify the allowance is updated
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Value(1).Object().Value("id").Number().IsEqual(1)
allowances.Value(1).Object().Value("progress").Number().InDelta(10.0, 0.01)
// Verify the history is updated
history := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
history.Length().IsEqual(1)
history.Value(0).Object().Value("allowance").Number().InDelta(10.0, 0.01)
history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
history.Value(0).Object().Value("description").String().IsEqual("Added to allowance 1")
}
func TestAddAllowanceWithSpillage(t *testing.T) {
e := startServer(t)
createTestAllowance(e, "Test Allowance 1", 5, 1)
createTestAllowance(e, "Test Allowance 2", 5, 1)
e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{Weight: 1}).Expect().Status(200)
request := map[string]interface{}{
"amount": 10,
"description": "Added to allowance 1",
}
e.POST("/user/1/allowance/1/add").WithJSON(request).Expect().Status(200)
// Verify the allowance is updated
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Value(1).Object().Value("id").Number().IsEqual(1)
allowances.Value(1).Object().Value("progress").Number().InDelta(5.0, 0.01)
allowances.Value(2).Object().Value("id").Number().IsEqual(2)
allowances.Value(2).Object().Value("progress").Number().InDelta(2.5, 0.01)
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
allowances.Value(0).Object().Value("progress").Number().InDelta(2.5, 0.01)
// Verify the history is updated
history := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
history.Length().IsEqual(1)
history.Value(0).Object().Value("allowance").Number().InDelta(10.0, 0.01)
history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
history.Value(0).Object().Value("description").String().IsEqual("Added to allowance 1")
}
func TestAddAllowanceIdZero(t *testing.T) {
e := startServer(t)
createTestAllowance(e, "Test Allowance 1", 1000, 1)
request := map[string]interface{}{
"amount": 10,
"description": "Added to allowance 1",
}
e.POST("/user/1/allowance/0/add").WithJSON(request).Expect().Status(200)
// Verify the allowance is updated
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
allowances.Value(0).Object().Value("progress").Number().InDelta(10.0, 0.01)
// Verify the history is updated
history := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
history.Length().IsEqual(1)
history.Value(0).Object().Value("allowance").Number().InDelta(10.0, 0.01)
history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
history.Value(0).Object().Value("description").String().IsEqual("Added to allowance 1")
}
func TestSubtractAllowanceSimple(t *testing.T) {
e := startServer(t)
createTestAllowance(e, "Test Allowance 1", 1000, 1)
request := map[string]interface{}{
"amount": 10,
"description": "Added to allowance 1",
}
e.POST("/user/1/allowance/1/add").WithJSON(request).Expect().Status(200)
request["amount"] = -2.5
e.POST("/user/1/allowance/1/add").WithJSON(request).Expect().Status(200)
// Verify the allowance is updated
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Value(1).Object().Value("id").Number().IsEqual(1)
allowances.Value(1).Object().Value("progress").Number().InDelta(7.5, 0.01)
// Verify the history is updated
history := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
history.Length().IsEqual(2)
history.Value(0).Object().Value("allowance").Number().InDelta(10.0, 0.01)
history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
history.Value(0).Object().Value("description").String().IsEqual("Added to allowance 1")
history.Value(1).Object().Value("allowance").Number().InDelta(-2.5, 0.01)
history.Value(1).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
history.Value(1).Object().Value("description").String().IsEqual("Added to allowance 1")
}
func TestSubtractllowanceIdZero(t *testing.T) {
e := startServer(t)
createTestAllowance(e, "Test Allowance 1", 1000, 1)
request := map[string]interface{}{
"amount": 10,
"description": "Added to allowance 1",
}
e.POST("/user/1/allowance/0/add").WithJSON(request).Expect().Status(200)
request["amount"] = -2.5
e.POST("/user/1/allowance/0/add").WithJSON(request).Expect().Status(200)
// Verify the allowance is updated
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
allowances.Value(0).Object().Value("progress").Number().InDelta(7.5, 0.01)
// Verify the history is updated
history := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
history.Length().IsEqual(2)
history.Value(0).Object().Value("allowance").Number().InDelta(10.0, 0.01)
history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
history.Value(0).Object().Value("description").String().IsEqual("Added to allowance 1")
history.Value(1).Object().Value("allowance").Number().InDelta(-2.5, 0.01)
history.Value(1).Object().Value("description").String().IsEqual("Added to allowance 1")
}
func getDelta(base time.Time, delta float64) (time.Time, time.Time) {
start := base.Add(-time.Duration(delta) * time.Second)
end := base.Add(time.Duration(delta) * time.Second)
return start, end
}
func createTestAllowance(e *httpexpect.Expect, name string, target float64, weight float64) {
e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
Name: name,
Target: target,
Weight: weight,
}).Expect().Status(201)
}
func createTestTask(e *httpexpect.Expect) int {
return createTestTaskWithAmount(e, 100)
} }

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -2,9 +2,7 @@ package main
import ( import (
"errors" "errors"
"fmt"
"log" "log"
"math"
"time" "time"
"gitea.seeseepuff.be/seeseemelk/mysqlite" "gitea.seeseepuff.be/seeseemelk/mysqlite"
@@ -51,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
} }
@@ -71,66 +67,27 @@ func (db *Db) UserExists(userId int) (bool, error) {
return count > 0, nil return count > 0, nil
} }
func (db *Db) GetUserAllowances(userId int) ([]Allowance, error) { func (db *Db) GetUserGoals(userId int) ([]Goal, error) {
allowances := make([]Allowance, 0) goals := make([]Goal, 0)
var err error var err error
var progress int64
totalAllowance := Allowance{} for row := range db.db.Query("select id, name, target, progress, weight from goals where user_id = ?").
err = db.db.Query("select balance, weight from users where id = ?").Bind(userId).ScanSingle(&progress, &totalAllowance.Weight)
if err != nil {
return nil, err
}
totalAllowance.Progress = float64(progress) / 100.0
allowances = append(allowances, totalAllowance)
for row := range db.db.Query("select id, name, target, balance, weight, colour from allowances where user_id = ?").
Bind(userId).Range(&err) { Bind(userId).Range(&err) {
allowance := Allowance{} goal := Goal{}
var target, progress, colour int err = row.Scan(&goal.ID, &goal.Name, &goal.Target, &goal.Progress, &goal.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
} }
allowances = append(allowances, allowance) goals = append(goals, goal)
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
return allowances, nil return goals, nil
} }
func (db *Db) GetUserAllowanceById(userId int, allowanceId int) (*Allowance, error) { func (db *Db) CreateGoal(userId int, goal *CreateGoalRequest) (int, error) {
allowance := &Allowance{} // Check if user exists before attempting to create a goal
if allowanceId == 0 {
var progress int64
err := db.db.Query("select balance, weight from users where id = ?").
Bind(userId).ScanSingle(&progress, &allowance.Weight)
allowance.Progress = float64(progress) / 100.0
if err != nil {
return nil, err
}
} else {
var target, progress int64
var colour int
err := db.db.Query("select id, name, target, balance, weight, colour from allowances where user_id = ? and id = ?").
Bind(userId, allowanceId).
ScanSingle(&allowance.ID, &allowance.Name, &target, &progress, &allowance.Weight, &colour)
allowance.Target = float64(target) / 100.0
allowance.Progress = float64(progress) / 100.0
allowance.Colour = ConvertColourToString(colour)
if err != nil {
return nil, err
}
}
return allowance, nil
}
func (db *Db) CreateAllowance(userId int, allowance *CreateAllowanceRequest) (int, error) {
// Check if user exists before attempting to create an allowance
exists, err := db.UserExists(userId) exists, err := db.UserExists(userId)
if err != nil { if err != nil {
return 0, err return 0, err
@@ -145,15 +102,9 @@ func (db *Db) CreateAllowance(userId int, allowance *CreateAllowanceRequest) (in
} }
defer tx.MustRollback() defer tx.MustRollback()
// Convert string colour to a valid hex format // Insert the new goal
colour, err := ConvertStringToColour(allowance.Colour) err = tx.Query("insert into goals (user_id, name, target, progress, weight) values (?, ?, ?, 0, ?)").
if err != nil { Bind(userId, goal.Name, goal.Target, goal.Weight).
return 0, err
}
// Insert the new allowance
err = tx.Query("insert into allowances (user_id, name, target, weight, colour) values (?, ?, ?, ?, ?)").
Bind(userId, allowance.Name, int(math.Round(allowance.Target*100.0)), allowance.Weight, colour).
Exec() Exec()
if err != nil { if err != nil {
@@ -176,21 +127,21 @@ func (db *Db) CreateAllowance(userId int, allowance *CreateAllowanceRequest) (in
return lastId, nil return lastId, nil
} }
func (db *Db) DeleteAllowance(userId int, allowanceId int) error { func (db *Db) DeleteGoal(userId int, goalId int) error {
// Check if the allowance exists for the user // Check if the goal exists for the user
count := 0 count := 0
err := db.db.Query("select count(*) from allowances where id = ? and user_id = ?"). err := db.db.Query("select count(*) from goals where id = ? and user_id = ?").
Bind(allowanceId, userId).ScanSingle(&count) Bind(goalId, userId).ScanSingle(&count)
if err != nil { if err != nil {
return err return err
} }
if count == 0 { if count == 0 {
return errors.New("allowance not found") return errors.New("goal not found")
} }
// Delete the allowance // Delete the goal
err = db.db.Query("delete from allowances where id = ? and user_id = ?"). err = db.db.Query("delete from goals where id = ? and user_id = ?").
Bind(allowanceId, userId).Exec() Bind(goalId, userId).Exec()
if err != nil { if err != nil {
return err return err
} }
@@ -198,114 +149,6 @@ func (db *Db) DeleteAllowance(userId int, allowanceId int) error {
return nil return nil
} }
func (db *Db) CompleteAllowance(userId int, allowanceId int) error {
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
// Get the cost of the allowance
var cost int
var allowanceName string
err = tx.Query("select balance, name from allowances where id = ? and user_id = ?").
Bind(allowanceId, userId).ScanSingle(&cost, &allowanceName)
if err != nil {
return err
}
// Delete the allowance
err = tx.Query("delete from allowances where id = ? and user_id = ?").
Bind(allowanceId, userId).Exec()
if err != nil {
return err
}
// Add a history entry
err = tx.Query("insert into history (user_id, timestamp, amount, description) values (?, ?, ?, ?)").
Bind(userId, time.Now().Unix(), -cost, fmt.Sprintf("Allowance completed: %s", allowanceName)).
Exec()
if err != nil {
return err
}
return tx.Commit()
}
func (db *Db) UpdateUserAllowance(userId int, allowance *UpdateAllowanceRequest) error {
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
err = tx.Query("update users set weight=? where id = ?").
Bind(allowance.Weight, userId).
Exec()
if err != nil {
return err
}
return tx.Commit()
}
func (db *Db) UpdateAllowance(userId int, allowanceId int, allowance *UpdateAllowanceRequest) error {
// Check if the allowance exists for the user
count := 0
err := db.db.Query("select count(*) from allowances where id = ? and user_id = ?").
Bind(allowanceId, userId).ScanSingle(&count)
if err != nil {
return err
}
if count == 0 {
return errors.New("allowance not found")
}
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
colour, err := ConvertStringToColour(allowance.Colour)
if err != nil {
return err
}
target := int(math.Round(allowance.Target * 100.0))
err = tx.Query("update allowances set name=?, target=?, weight=?, colour=? where id = ? and user_id = ?").
Bind(allowance.Name, target, allowance.Weight, colour, allowanceId, userId).
Exec()
if err != nil {
return err
}
return tx.Commit()
}
func (db *Db) BulkUpdateAllowance(userId int, allowances []BulkUpdateAllowanceRequest) error {
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
for _, allowance := range allowances {
if allowance.ID == 0 {
err = tx.Query("update users set weight=? where id = ?").
Bind(allowance.Weight, userId).
Exec()
} else {
err = tx.Query("update allowances set weight=? where id = ? and user_id = ?").
Bind(allowance.Weight, allowance.ID, userId).
Exec()
}
if err != nil {
return err
}
}
return tx.Commit()
}
func (db *Db) CreateTask(task *CreateTaskRequest) (int, error) { func (db *Db) CreateTask(task *CreateTaskRequest) (int, error) {
tx, err := db.db.Begin() tx, err := db.db.Begin()
if err != nil { if err != nil {
@@ -314,9 +157,8 @@ func (db *Db) CreateTask(task *CreateTaskRequest) (int, error) {
defer tx.MustRollback() defer tx.MustRollback()
// 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) values (?, ?, ?)").
Bind(task.Name, reward, task.Assigned). Bind(task.Name, task.Reward, task.Assigned).
Exec() Exec()
if err != nil { if err != nil {
@@ -345,9 +187,7 @@ func (db *Db) GetTasks() ([]Task, error) {
for row := range db.db.Query("select id, name, reward, assigned from tasks").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.Reward = float64(reward) / 100.0
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -362,31 +202,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{}
var reward int64
err := db.db.Query("select id, name, reward, assigned from tasks where id = ?"). err := db.db.Query("select id, name, reward, assigned from tasks where id = ?").
Bind(id).ScanSingle(&task.ID, &task.Name, &reward, &task.Assigned) Bind(id).ScanSingle(&task.ID, &task.Name, &task.Reward, &task.Assigned)
task.Reward = float64(reward) / 100.0
if err != nil { if err != nil {
return Task{}, err return Task{}, err
} }
return task, nil return task, nil
} }
func (db *Db) DeleteTask(id int) error {
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
err = tx.Query("delete from tasks where id = ?").Bind(id).Exec()
if err != nil {
return err
}
return tx.Commit()
}
func (db *Db) HasTask(id int) (bool, error) { func (db *Db) HasTask(id int) (bool, error) {
count := 0 count := 0
err := db.db.Query("select count(*) from tasks where id = ?"). err := db.db.Query("select count(*) from tasks where id = ?").
@@ -404,9 +227,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
@@ -414,106 +236,15 @@ func (db *Db) UpdateTask(id int, task *CreateTaskRequest) error {
return tx.Commit() return tx.Commit()
} }
func (db *Db) CompleteTask(taskId int) error { func (db *Db) AddAllowance(userId int, allowance *PostAllowance) error {
tx, err := db.db.Begin() tx, err := db.db.Begin()
if err != nil { if err != nil {
return err return err
} }
defer tx.MustRollback() defer tx.MustRollback()
var reward int err = tx.Query("insert into history (user_id, timestamp, amount) values (?, ?, ?)").
var rewardName string Bind(userId, time.Now().Unix(), allowance.Allowance).
err = tx.Query("select reward, name from tasks where id = ?").Bind(taskId).ScanSingle(&reward, &rewardName)
if err != nil {
return err
}
for userRow := range tx.Query("select id from users").Range(&err) {
var userId int
err = userRow.Scan(&userId)
if err != nil {
return err
}
// Add the history entry
err = tx.Query("insert into history (user_id, timestamp, amount, description) values (?, ?, ?, ?)").
Bind(userId, time.Now().Unix(), reward, fmt.Sprintf("Task completed: %s", rewardName)).
Exec()
if err != nil {
return err
}
err := db.addDistributedReward(tx, userId, reward)
if err != nil {
return err
}
}
if err != nil {
return err
}
// Remove the task
err = tx.Query("delete from tasks where id = ?").Bind(taskId).Exec()
return tx.Commit()
}
func (db *Db) addDistributedReward(tx *mysqlite.Tx, userId int, reward int) error {
var userWeight float64
err := tx.Query("select weight from users where id = ?").Bind(userId).ScanSingle(&userWeight)
if err != nil {
return err
}
// Calculate the sums of all weights
var sumOfWeights float64
err = tx.Query("select sum(weight) from allowances where user_id = ? and weight > 0").Bind(userId).ScanSingle(&sumOfWeights)
sumOfWeights += userWeight
remainingReward := reward
if sumOfWeights > 0 {
// Distribute the reward to the allowances
for allowanceRow := range tx.Query("select id, weight, target, balance from allowances where user_id = ? and weight > 0 order by (target - balance) asc").Bind(userId).Range(&err) {
var allowanceId, allowanceTarget, allowanceBalance int
var allowanceWeight float64
err = allowanceRow.Scan(&allowanceId, &allowanceWeight, &allowanceTarget, &allowanceBalance)
if err != nil {
return err
}
// Calculate the amount to add to the allowance
amount := int((allowanceWeight / sumOfWeights) * float64(remainingReward))
if allowanceBalance+amount > allowanceTarget {
// If the amount reaches past the target, set it to the target
amount = allowanceTarget - allowanceBalance
}
sumOfWeights -= allowanceWeight
err = tx.Query("update allowances set balance = balance + ? where id = ? and user_id = ?").
Bind(amount, allowanceId, userId).Exec()
if err != nil {
return err
}
remainingReward -= amount
}
}
// Add the remaining reward to the user
err = tx.Query("update users set balance = balance + ? where id = ?").
Bind(remainingReward, userId).Exec()
return err
}
func (db *Db) AddHistory(userId int, allowance *PostHistory) error {
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
amount := int(math.Round(allowance.Allowance * 100.0))
err = tx.Query("insert into history (user_id, timestamp, amount, description) values (?, ?, ?, ?)").
Bind(userId, time.Now().Unix(), amount, allowance.Description).
Exec() Exec()
if err != nil { if err != nil {
return err return err
@@ -521,20 +252,17 @@ func (db *Db) AddHistory(userId int, allowance *PostHistory) error {
return tx.Commit() return tx.Commit()
} }
func (db *Db) GetHistory(userId int) ([]History, error) { func (db *Db) GetHistory(userId int) ([]Allowance, error) {
history := make([]History, 0) history := make([]Allowance, 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 from history where user_id = ? order by `timestamp` desc").
Bind(userId).Range(&err) { Bind(userId).Range(&err) {
allowance := History{} allowance := Allowance{}
var timestamp, amount int64 err = row.Scan(&allowance.Allowance)
err = row.Scan(&amount, &timestamp, &allowance.Description)
if err != nil { if err != nil {
return nil, err return nil, err
} }
allowance.Allowance = float64(amount) / 100.0
allowance.Timestamp = time.Unix(timestamp, 0)
history = append(history, allowance) history = append(history, allowance)
} }
if err != nil { if err != nil {
@@ -542,92 +270,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()
}

View File

@@ -8,56 +8,40 @@ 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 Allowance struct {
Allowance float64 `json:"allowance"` Allowance int `json:"allowance"`
Timestamp time.Time `json:"timestamp"` Timestamp time.Time
Description string `json:"description"`
} }
type PostHistory struct { type PostAllowance 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"` // Pointer to allow null Assigned *int `json:"assigned"` // Pointer to allow null
} }
type Allowance struct { type Goal 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 int `json:"weight"`
Colour string `json:"colour"`
} }
type CreateAllowanceRequest struct { type CreateGoalRequest struct {
Name string `json:"name"` Name string `json:"name"`
Target float64 `json:"target"` Target int `json:"target"`
Weight float64 `json:"weight"` Weight int `json:"weight"`
Colour string `json:"colour"`
}
type UpdateAllowanceRequest struct {
Name string `json:"name"`
Target float64 `json:"target"`
Weight float64 `json:"weight"`
Colour string `json:"colour"`
}
type BulkUpdateAllowanceRequest struct {
ID int `json:"id"`
Weight float64 `json:"weight"`
} }
type CreateGoalResponse struct { type CreateGoalResponse struct {
@@ -65,16 +49,11 @@ 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"`
} }
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"`
}

View File

@@ -3,9 +3,8 @@ module allowance_planner
go 1.24.2 go 1.24.2
require ( require (
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0 gitea.seeseepuff.be/seeseemelk/mysqlite v0.11.1
github.com/gavv/httpexpect/v2 v2.17.0 github.com/gavv/httpexpect/v2 v2.17.0
github.com/gin-contrib/cors v1.7.5
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0
) )
@@ -68,9 +67,9 @@ require (
gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.65.7 // indirect modernc.org/libc v1.65.2 // 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.10.0 // indirect
modernc.org/sqlite v1.37.0 // indirect modernc.org/sqlite v1.37.0 // indirect
moul.io/http2curl/v2 v2.3.0 // indirect moul.io/http2curl/v2 v2.3.0 // indirect
zombiezen.com/go/sqlite v1.4.0 // indirect zombiezen.com/go/sqlite v1.4.0 // indirect

View File

@@ -1,5 +1,5 @@
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0 h1:aRItVfUj48fBmuec7rm/jY9KCfvHW2VzJfItVk4t8sw= gitea.seeseepuff.be/seeseemelk/mysqlite v0.11.1 h1:5s0r2IRpomGJC6pjirdMk7HAcAYEydLK5AhBZy+V1Ys=
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE= gitea.seeseepuff.be/seeseemelk/mysqlite v0.11.1/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4= github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4=
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
@@ -28,8 +28,6 @@ github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBv
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gavv/httpexpect/v2 v2.17.0 h1:nIJqt5v5e4P7/0jODpX2gtSw+pHXUqdP28YcjqwDZmE= github.com/gavv/httpexpect/v2 v2.17.0 h1:nIJqt5v5e4P7/0jODpX2gtSw+pHXUqdP28YcjqwDZmE=
github.com/gavv/httpexpect/v2 v2.17.0/go.mod h1:E8ENFlT9MZ3Si2sfM6c6ONdwXV2noBCGkhA+lkJgkP0= github.com/gavv/httpexpect/v2 v2.17.0/go.mod h1:E8ENFlT9MZ3Si2sfM6c6ONdwXV2noBCGkhA+lkJgkP0=
github.com/gin-contrib/cors v1.7.5 h1:cXC9SmofOrRg0w9PigwGlHG3ztswH6bqq4vJVXnvYMk=
github.com/gin-contrib/cors v1.7.5/go.mod h1:4q3yi7xBEDDWKapjT2o1V7mScKDDr8k+jZ0fSquGoy0=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
@@ -70,9 +68,8 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
@@ -103,8 +100,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg= github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg=
github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
@@ -193,9 +188,8 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
@@ -208,18 +202,18 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s= modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= modernc.org/ccgo/v4 v4.27.1 h1:emhLB4uoOmkZUnTDFcMI3AbkmU/Evjuerit9Taqe6Ss=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= modernc.org/ccgo/v4 v4.27.1/go.mod h1:543Q0qQhJWekKVS5P6yL5fO6liNhla9Lbm2/B3rEKDE=
modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8= modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8=
modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00= modernc.org/libc v1.65.2 h1:drWL1QO9fKXr3kXDN8y+4lKyBr8bA3mtUBQpftq3IJw=
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU= modernc.org/libc v1.65.2/go.mod h1:VI3V2S5mNka4deJErQ0jsMXe7jgxojE2fOB/mWoHlbc=
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.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=

View File

@@ -11,7 +11,6 @@ import (
"os" "os"
"strconv" "strconv"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -76,7 +75,7 @@ func getUser(c *gin.Context) {
c.IndentedJSON(http.StatusOK, user) c.IndentedJSON(http.StatusOK, user)
} }
func getUserAllowance(c *gin.Context) { func getUserGoals(c *gin.Context) {
userIdStr := c.Param("userId") userIdStr := c.Param("userId")
userId, err := strconv.Atoi(userIdStr) userId, err := strconv.Atoi(userIdStr)
if err != nil { if err != nil {
@@ -97,59 +96,16 @@ func getUserAllowance(c *gin.Context) {
return return
} }
allowances, err := db.GetUserAllowances(userId) goals, err := db.GetUserGoals(userId)
if err != nil { if err != nil {
log.Printf("Error getting user allowance: %v", err) log.Printf("Error getting user goals: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return return
} }
c.IndentedJSON(http.StatusOK, allowances) c.IndentedJSON(http.StatusOK, goals)
} }
func getUserAllowanceById(c *gin.Context) { func createUserGoal(c *gin.Context) {
userIdStr := c.Param("userId")
allowanceIdStr := c.Param("allowanceId")
userId, err := strconv.Atoi(userIdStr)
if err != nil {
log.Printf(ErrInvalidUserID+": %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID})
return
}
allowanceId, err := strconv.Atoi(allowanceIdStr)
if err != nil {
log.Printf("Invalid allowance ID: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid allowance ID"})
return
}
exists, err := db.UserExists(userId)
if err != nil {
log.Printf(ErrCheckingUserExist, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
if !exists {
c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound})
return
}
allowance, err := db.GetUserAllowanceById(userId, allowanceId)
if errors.Is(err, mysqlite.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "Allowance not found"})
return
}
if err != nil {
log.Printf("Error getting allowance: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
c.IndentedJSON(http.StatusOK, allowance)
}
func createUserAllowance(c *gin.Context) {
userIdStr := c.Param("userId") userIdStr := c.Param("userId")
userId, err := strconv.Atoi(userIdStr) userId, err := strconv.Atoi(userIdStr)
if err != nil { if err != nil {
@@ -159,7 +115,7 @@ func createUserAllowance(c *gin.Context) {
} }
// Parse request body // Parse request body
var goalRequest CreateAllowanceRequest var goalRequest CreateGoalRequest
if err := c.ShouldBindJSON(&goalRequest); err != nil { if err := c.ShouldBindJSON(&goalRequest); err != nil {
log.Printf("Error parsing request body: %v", err) log.Printf("Error parsing request body: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
@@ -168,12 +124,12 @@ func createUserAllowance(c *gin.Context) {
// Validate request // Validate request
if goalRequest.Name == "" { if goalRequest.Name == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Allowance name cannot be empty"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Goal name cannot be empty"})
return return
} }
// Create goal in database // Create goal in database
goalId, err := db.CreateAllowance(userId, &goalRequest) goalId, err := db.CreateGoal(userId, &goalRequest)
if err != nil { if err != nil {
log.Printf("Error creating goal: %v", err) log.Printf("Error creating goal: %v", err)
if err.Error() == "user does not exist" { if err.Error() == "user does not exist" {
@@ -189,8 +145,9 @@ func createUserAllowance(c *gin.Context) {
c.IndentedJSON(http.StatusCreated, response) c.IndentedJSON(http.StatusCreated, response)
} }
func bulkPutUserAllowance(c *gin.Context) { func deleteUserGoal(c *gin.Context) {
userIdStr := c.Param("userId") userIdStr := c.Param("userId")
goalIdStr := c.Param("goalId")
userId, err := strconv.Atoi(userIdStr) userId, err := strconv.Atoi(userIdStr)
if err != nil { if err != nil {
@@ -199,6 +156,13 @@ func bulkPutUserAllowance(c *gin.Context) {
return return
} }
goalId, err := strconv.Atoi(goalIdStr)
if err != nil {
log.Printf("Invalid goal ID: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid goal ID"})
return
}
exists, err := db.UserExists(userId) exists, err := db.UserExists(userId)
if err != nil { if err != nil {
log.Printf(ErrCheckingUserExist, err) log.Printf(ErrCheckingUserExist, err)
@@ -210,212 +174,18 @@ func bulkPutUserAllowance(c *gin.Context) {
return return
} }
var allowanceRequest []BulkUpdateAllowanceRequest err = db.DeleteGoal(userId, goalId)
if err := c.ShouldBindJSON(&allowanceRequest); err != nil {
log.Printf("Error parsing request body: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
err = db.BulkUpdateAllowance(userId, allowanceRequest)
if err != nil { if err != nil {
log.Printf("Error updating allowance: %v", err) if err.Error() == "goal not found" {
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"})
return
}
c.IndentedJSON(http.StatusOK, gin.H{"message": "Allowance updated successfully"})
}
func deleteUserAllowance(c *gin.Context) {
userIdStr := c.Param("userId")
allowanceIdStr := c.Param("allowanceId")
userId, err := strconv.Atoi(userIdStr)
if err != nil {
log.Printf(ErrInvalidUserID+": %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID})
return
}
allowanceId, err := strconv.Atoi(allowanceIdStr)
if err != nil {
log.Printf("Invalid allowance ID: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid allowance ID"})
return
}
if allowanceId == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Allowance id zero cannot be deleted"})
return
}
exists, err := db.UserExists(userId)
if err != nil {
log.Printf(ErrCheckingUserExist, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
if !exists {
c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound})
return
}
err = db.DeleteAllowance(userId, allowanceId)
if err != nil {
if err.Error() == "allowance not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "History not found"})
} else { } else {
log.Printf("Error deleting allowance: %v", err) log.Printf("Error deleting goal: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
} }
return return
} }
c.IndentedJSON(http.StatusOK, gin.H{"message": "History deleted successfully"}) c.IndentedJSON(http.StatusOK, gin.H{"message": "Goal deleted successfully"})
}
func putUserAllowance(c *gin.Context) {
userIdStr := c.Param("userId")
allowanceIdStr := c.Param("allowanceId")
userId, err := strconv.Atoi(userIdStr)
if err != nil {
log.Printf(ErrInvalidUserID+": %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID})
return
}
allowanceId, err := strconv.Atoi(allowanceIdStr)
if err != nil {
log.Printf("Invalid allowance ID: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid allowance ID"})
return
}
exists, err := db.UserExists(userId)
if err != nil {
log.Printf(ErrCheckingUserExist, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
if !exists {
c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound})
return
}
var allowanceRequest UpdateAllowanceRequest
if err := c.ShouldBindJSON(&allowanceRequest); err != nil {
log.Printf("Error parsing request body: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
if allowanceId == 0 {
err = db.UpdateUserAllowance(userId, &allowanceRequest)
} else {
err = db.UpdateAllowance(userId, allowanceId, &allowanceRequest)
}
if err != nil {
log.Printf("Error updating allowance: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
c.IndentedJSON(http.StatusOK, gin.H{"message": "Allowance updated successfully"})
}
func completeAllowance(c *gin.Context) {
userIdStr := c.Param("userId")
allowanceIdStr := c.Param("allowanceId")
userId, err := strconv.Atoi(userIdStr)
if err != nil {
log.Printf(ErrInvalidUserID+": %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID})
return
}
allowanceId, err := strconv.Atoi(allowanceIdStr)
if err != nil {
log.Printf("Invalid allowance ID: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid allowance ID"})
return
}
exists, err := db.UserExists(userId)
if err != nil {
log.Printf(ErrCheckingUserExist, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
if !exists {
c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound})
return
}
err = db.CompleteAllowance(userId, allowanceId)
if errors.Is(err, mysqlite.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "Allowance not found"})
return
}
if err != nil {
log.Printf("Error completing allowance: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
c.IndentedJSON(http.StatusOK, gin.H{"message": "Allowance completed successfully"})
}
func addToAllowance(c *gin.Context) {
userIdStr := c.Param("userId")
allowanceIdStr := c.Param("allowanceId")
userId, err := strconv.Atoi(userIdStr)
if err != nil {
log.Printf(ErrInvalidUserID+": %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID})
return
}
allowanceId, err := strconv.Atoi(allowanceIdStr)
if err != nil {
log.Printf("Invalid allowance ID: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid allowance ID"})
return
}
exists, err := db.UserExists(userId)
if err != nil {
log.Printf(ErrCheckingUserExist, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
if !exists {
c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound})
return
}
var allowanceRequest AddAllowanceAmountRequest
if err := c.ShouldBindJSON(&allowanceRequest); err != nil {
log.Printf("Error parsing request body: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
err = db.AddAllowanceAmount(userId, allowanceId, allowanceRequest)
if errors.Is(err, mysqlite.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "Allowance not found"})
return
}
if err != nil {
log.Printf("Error completing allowance: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
c.IndentedJSON(http.StatusOK, gin.H{"message": "Allowance completed successfully"})
} }
func createTask(c *gin.Context) { func createTask(c *gin.Context) {
@@ -519,61 +289,7 @@ func putTask(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Task updated successfully"}) c.JSON(http.StatusOK, gin.H{"message": "Task updated successfully"})
} }
func deleteTask(c *gin.Context) { func postAllowance(c *gin.Context) {
taskIdStr := c.Param("taskId")
taskId, err := strconv.Atoi(taskIdStr)
if err != nil {
log.Printf("Invalid task ID: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
return
}
hasTask, err := db.HasTask(taskId)
if err != nil {
log.Printf("Error checking task existence: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
if !hasTask {
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
return
}
err = db.DeleteTask(taskId)
if err != nil {
log.Printf("Error deleting task: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Task deleted successfully"})
}
func completeTask(c *gin.Context) {
taskIdStr := c.Param("taskId")
taskId, err := strconv.Atoi(taskIdStr)
if err != nil {
log.Printf("Invalid task ID: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
return
}
err = db.CompleteTask(taskId)
if errors.Is(err, mysqlite.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
return
}
if err != nil {
log.Printf("Error completing task: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Task completed successfully"})
}
func postHistory(c *gin.Context) {
userIdStr := c.Param("userId") userIdStr := c.Param("userId")
userId, err := strconv.Atoi(userIdStr) userId, err := strconv.Atoi(userIdStr)
if err != nil { if err != nil {
@@ -582,18 +298,13 @@ func postHistory(c *gin.Context) {
return return
} }
var historyRequest PostHistory var allowanceRequest PostAllowance
if err := c.ShouldBindJSON(&historyRequest); err != nil { if err := c.ShouldBindJSON(&allowanceRequest); err != nil {
log.Printf("Error parsing request body: %v", err) log.Printf("Error parsing request body: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return return
} }
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)
@@ -605,16 +316,16 @@ func postHistory(c *gin.Context) {
return return
} }
err = db.AddHistory(userId, &historyRequest) err = db.AddAllowance(userId, &allowanceRequest)
if err != nil { if err != nil {
log.Printf("Error updating history: %v", err) log.Printf("Error updating allowance: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError}) c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return return
} }
c.JSON(http.StatusOK, gin.H{"message": "History updated successfully"}) c.JSON(http.StatusOK, gin.H{"message": "Allowance updated successfully"})
} }
func getHistory(c *gin.Context) { func getAllowance(c *gin.Context) {
userIdStr := c.Param("userId") userIdStr := c.Param("userId")
userId, err := strconv.Atoi(userIdStr) userId, err := strconv.Atoi(userIdStr)
if err != nil { if err != nil {
@@ -642,32 +353,17 @@ func start(ctx context.Context, config *ServerConfig) {
defer db.db.MustClose() defer db.db.MustClose()
router := gin.Default() router := gin.Default()
corsConfig := cors.DefaultConfig()
corsConfig.AllowAllOrigins = true
router.Use(cors.New(corsConfig))
// Web endpoints
loadWebEndpoints(router)
// API endpoints
router.GET("/api/users", getUsers) router.GET("/api/users", getUsers)
router.GET("/api/user/:userId", getUser) router.GET("/api/user/:userId", getUser)
router.POST("/api/user/:userId/history", postHistory) router.POST("/api/user/:userId/allowance", postAllowance)
router.GET("/api/user/:userId/history", getHistory) //router.GET("/api/user/:userId/allowance", getAllowance)
router.GET("/api/user/:userId/allowance", getUserAllowance) router.GET("/api/user/:userId/goals", getUserGoals)
router.POST("/api/user/:userId/allowance", createUserAllowance) router.POST("/api/user/:userId/goals", createUserGoal)
router.PUT("/api/user/:userId/allowance", bulkPutUserAllowance) router.DELETE("/api/user/:userId/goal/:goalId", deleteUserGoal)
router.GET("/api/user/:userId/allowance/:allowanceId", getUserAllowanceById)
router.DELETE("/api/user/:userId/allowance/:allowanceId", deleteUserAllowance)
router.PUT("/api/user/:userId/allowance/:allowanceId", putUserAllowance)
router.POST("/api/user/:userId/allowance/:allowanceId/complete", completeAllowance)
router.POST("/api/user/:userId/allowance/:allowanceId/add", addToAllowance)
router.POST("/api/tasks", createTask) router.POST("/api/tasks", createTask)
router.GET("/api/tasks", getTasks) router.GET("/api/tasks", getTasks)
router.GET("/api/task/:taskId", getTask) router.GET("/api/task/:taskId", getTask)
router.PUT("/api/task/:taskId", putTask) router.PUT("/api/task/:taskId", putTask)
router.DELETE("/api/task/:taskId", deleteTask)
router.POST("/api/task/:taskId/complete", completeTask)
srv := &http.Server{ srv := &http.Server{
Addr: config.Addr, Addr: config.Addr,
@@ -700,11 +396,6 @@ func start(ctx context.Context, config *ServerConfig) {
func main() { func main() {
config := ServerConfig{ config := ServerConfig{
Datasource: os.Getenv("DB_PATH"), Datasource: os.Getenv("DB_PATH"),
Addr: ":8080",
}
if config.Datasource == "" {
config.Datasource = "allowance_planner.db3"
log.Printf("Warning: No DB_PATH set, using default of %s", config.Datasource)
} }
start(context.Background(), &config) start(context.Background(), &config)
} }

View File

@@ -1,9 +1,7 @@
create table users create table users
( (
id integer primary key, id integer primary key,
name text not null, name text not null
weight real not null default 10.0,
balance integer not null default 0
) strict; ) strict;
create table history create table history
@@ -14,13 +12,13 @@ create table history
amount integer not null amount integer not null
); );
create table allowances create table goals
( (
id integer primary key, id integer primary key,
user_id integer not null, user_id integer not null,
name text not null, name text not null,
target integer not null, target integer not null,
balance integer not null default 0, progress integer not null,
weight real not null weight real not null
); );

View File

@@ -1,2 +0,0 @@
alter table allowances
add column colour integer;

View File

@@ -1 +0,0 @@
update users set weight = 10.0 where weight = 0.0;

View File

@@ -1,2 +0,0 @@
alter table history
add column description text;

View File

@@ -1,228 +0,0 @@
package main
import (
"errors"
"github.com/gin-gonic/gin"
"net/http"
"strconv"
)
type ViewModel struct {
Users []User
CurrentUser int
Allowances []Allowance
Tasks []Task
History []History
Error string
}
func loadWebEndpoints(router *gin.Engine) {
router.LoadHTMLFiles("web.gohtml")
router.GET("/", renderIndex)
router.GET("/login", renderLogin)
router.POST("/createTask", renderCreateTask)
router.GET("/completeTask", renderCompleteTask)
router.POST("/createAllowance", renderCreateAllowance)
router.GET("/completeAllowance", renderCompleteAllowance)
}
func renderLogin(c *gin.Context) {
if c.Query("user") != "" {
c.SetCookie("user", c.Query("user"), 3600, "/", "localhost", false, true)
}
c.Redirect(http.StatusFound, "/")
}
func renderIndex(c *gin.Context) {
currentUser := getCurrentUser(c)
if currentUser == nil {
return
}
renderWithUser(c, *currentUser)
}
func renderCreateTask(c *gin.Context) {
currentUser := getCurrentUser(c)
if currentUser == nil {
return
}
name := c.PostForm("name")
rewardStr := c.PostForm("reward")
reward, err := strconv.ParseFloat(rewardStr, 64)
if err != nil {
renderError(c, http.StatusBadRequest, err)
return
}
if name == "" || reward <= 0 {
renderError(c, http.StatusBadRequest, err)
return
}
_, err = db.CreateTask(&CreateTaskRequest{
Name: name,
Reward: reward,
})
if err != nil {
renderError(c, http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusFound, "/")
}
func renderCompleteTask(c *gin.Context) {
taskIDStr := c.Query("task")
taskID, err := strconv.Atoi(taskIDStr)
if err != nil {
renderError(c, http.StatusBadRequest, err)
return
}
err = db.CompleteTask(taskID)
if err != nil {
renderError(c, http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusFound, "/")
}
func renderCreateAllowance(c *gin.Context) {
currentUser := getCurrentUser(c)
if currentUser == nil {
return
}
name := c.PostForm("name")
targetStr := c.PostForm("target")
target, err := strconv.ParseFloat(targetStr, 64)
if err != nil {
renderError(c, http.StatusBadRequest, err)
return
}
weightStr := c.PostForm("weight")
weight, err := strconv.ParseFloat(weightStr, 64)
if err != nil {
renderError(c, http.StatusBadRequest, err)
return
}
if name == "" || target <= 0 || weight <= 0 {
renderError(c, http.StatusBadRequest, err)
return
}
_, err = db.CreateAllowance(*currentUser, &CreateAllowanceRequest{
Name: name,
Target: target,
Weight: weight,
})
if err != nil {
renderError(c, http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusFound, "/")
}
func renderCompleteAllowance(c *gin.Context) {
currentUser := getCurrentUser(c)
if currentUser == nil {
return
}
allowanceIDStr := c.Query("allowance")
allowanceID, err := strconv.Atoi(allowanceIDStr)
if err != nil {
renderError(c, http.StatusBadRequest, err)
return
}
err = db.CompleteAllowance(*currentUser, allowanceID)
if err != nil {
renderError(c, http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusFound, "/")
}
func getCurrentUser(c *gin.Context) *int {
currentUserStr, err := c.Cookie("user")
if errors.Is(err, http.ErrNoCookie) {
renderNoUser(c)
return nil
}
if err != nil {
unsetUserCookie(c)
return nil
}
currentUser, err := strconv.Atoi(currentUserStr)
if err != nil {
unsetUserCookie(c)
return nil
}
userExists, err := db.UserExists(currentUser)
if !userExists || err != nil {
unsetUserCookie(c)
return nil
}
return &currentUser
}
func unsetUserCookie(c *gin.Context) {
c.SetCookie("user", "", -1, "/", "localhost", false, true)
c.Redirect(http.StatusFound, "/")
}
func renderNoUser(c *gin.Context) {
users, err := db.GetUsers()
if err != nil {
renderError(c, http.StatusInternalServerError, err)
return
}
c.HTML(http.StatusOK, "web.gohtml", ViewModel{
Users: users,
})
}
func renderWithUser(c *gin.Context, currentUser int) {
users, err := db.GetUsers()
if err != nil {
renderError(c, http.StatusInternalServerError, err)
return
}
allowances, err := db.GetUserAllowances(currentUser)
if err != nil {
renderError(c, http.StatusInternalServerError, err)
return
}
tasks, err := db.GetTasks()
if err != nil {
renderError(c, http.StatusInternalServerError, err)
return
}
history, err := db.GetHistory(currentUser)
if err != nil {
renderError(c, http.StatusInternalServerError, err)
return
}
c.HTML(http.StatusOK, "web.gohtml", ViewModel{
Users: users,
CurrentUser: currentUser,
Allowances: allowances,
Tasks: tasks,
History: history,
})
}
func renderError(c *gin.Context, statusCode int, err error) {
c.HTML(statusCode, "web.gohtml", ViewModel{
Error: err.Error(),
})
}

View File

@@ -1,132 +0,0 @@
{{- /*gotype: allowance_planner.ViewModel*/}}
<html lang="en">
<head>
<title>Allowance Planner 2000</title>
<style>
tr:hover {
background-color: #f0f0f0;
}
</style>
</head>
<body>
<h1>Allowance Planner 2000</h1>
{{if ne .Error ""}}
<h2>Error</h2>
<p>{{.Error}}</p>
{{else}}
<h2>Users</h2>
{{range .Users}}
{{if eq $.CurrentUser .ID}}
<strong>{{.Name}}</strong>
{{else}}
<a href="/login?user={{.ID}}">{{.Name}}</a>
{{end}}
{{end}}
{{if ne .CurrentUser 0}}
<h2>Allowances</h2>
<form action="/createAllowance" method="post">
<table border="1">
<thead>
<tr>
<th>Name</th>
<th>Progress</th>
<th>Target</th>
<th>Weight</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td><label><input type="text" name="name" placeholder="Name"></label></td>
<td></td>
<td><label><input type="number" name="target" placeholder="Target"></label></td>
<td><label><input type="number" name="weight" placeholder="Weight"></label></td>
<td><button>Create</button></td>
</tr>
{{range .Allowances}}
{{if eq .ID 0}}
<tr>
<td>Total</td>
<td>{{.Progress}}</td>
<td></td>
<td>{{.Weight}}</td>
</tr>
{{else}}
<tr>
<td>{{.Name}}</td>
<td><progress max="{{.Target}}" value="{{.Progress}}"></progress> ({{.Progress}})</td>
<td>{{.Target}}</td>
<td>{{.Weight}}</td>
{{if ge .Progress .Target}}
<td>
<a href="/completeAllowance?allowance={{.ID}}">Mark as completed</a>
</td>
{{end}}
</tr>
{{end}}
{{end}}
</tbody>
</table>
</form>
<h2>Tasks</h2>
<form method="post" action="/createTask">
<table border="1">
<thead>
<tr>
<th>Name</th>
<th>Assigned</th>
<th>Reward</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{range .Tasks}}
<tr>
<td>{{.Name}}</td>
<td>
{{if eq .Assigned nil}}
None
{{else}}
{{.Assigned}}
{{end}}
</td>
<td>{{.Reward}}</td>
<td>
<a href="/completeTask?task={{.ID}}">Mark as completed</a>
</td>
</tr>
{{end}}
<tr>
<td><label><input type="text" name="name" placeholder="Name"></label></td>
<td></td>
<td><label><input type="number" name="reward" placeholder="Reward"></label></td>
<td><button>Create</button></td>
</tr>
</tbody>
</table>
</form>
<h2>History</h2>
<table border="1">
<thead>
<tr>
<th>Timestamp</th>
<th>Allowance</th>
</tr>
</thead>
<tbody>
{{range .History}}
<tr>
<td>{{.Timestamp}}</td>
<td>{{.Allowance}}</td>
</tr>
{{end}}
</tbody>
</table>
{{end}}
{{end}}
</body>
</html>

View File

@@ -59,7 +59,7 @@ paths:
404: 404:
description: The users could not be found. description: The users could not be found.
/user/{userId}/history: /user/{userId}/allowance:
get: get:
summary: Gets the allowance history of a user summary: Gets the allowance history of a user
parameters: parameters:
@@ -114,7 +114,7 @@ paths:
400: 400:
description: The allowance could not be updated. description: The allowance could not be updated.
/user/{userId}/allowance: /user/{userId}/goals:
get: get:
summary: Gets all goals summary: Gets all goals
parameters: parameters:
@@ -201,7 +201,7 @@ paths:
404: 404:
description: The goals could not be found. description: The goals could not be found.
/user/{userId}/allowance/{goalId}: /user/{userId}/goal/{goalId}:
get: get:
summary: Gets information about a goal summary: Gets information about a goal
parameters: parameters:
@@ -284,7 +284,7 @@ paths:
404: 404:
description: The goal could not be found. description: The goal could not be found.
/user/{userId}/allowance/{goalId}/complete: /user/{userId}/goal/{goalId}/complete:
post: post:
summary: Completes a goal. summary: Completes a goal.
description: Completes a goal. This will subtract this goal's value from the user's allowance and then remove the goal. description: Completes a goal. This will subtract this goal's value from the user's allowance and then remove the goal.

View File

@@ -1,16 +0,0 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
Chrome >=79
ChromeAndroid >=79
Firefox >=70
Edge >=79
Safari >=14
iOS >=14

View File

@@ -1,16 +0,0 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

View File

@@ -1,47 +0,0 @@
{
"root": true,
"ignorePatterns": ["projects/**/*"],
"overrides": [
{
"files": ["*.ts"],
"parserOptions": {
"project": ["tsconfig.json"],
"createDefaultProgram": true
},
"extends": [
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/prefer-standalone": "off",
"@angular-eslint/component-class-suffix": [
"error",
{
"suffixes": ["Page", "Component"]
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
],
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
]
}
},
{
"files": ["*.html"],
"extends": ["plugin:@angular-eslint/template/recommended"],
"rules": {}
}
]
}

View File

@@ -1,70 +0,0 @@
# Specifies intentionally untracked files to ignore when using Git
# http://git-scm.com/docs/gitignore
*~
*.sw[mnpcod]
.tmp
*.tmp
*.tmp.*
UserInterfaceState.xcuserstate
$RECYCLE.BIN/
*.log
log.txt
/.sourcemaps
/.versions
/coverage
# Ionic
/.ionic
/www
/platforms
/plugins
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-project
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular
/.angular/cache
.sass-cache/
/.nx
/.nx/cache
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

View File

@@ -1,5 +0,0 @@
{
"recommendations": [
"ionic.ionic"
]
}

View File

@@ -1,3 +0,0 @@
{
"typescript.preferences.autoImportFileExcludePatterns": ["@ionic/angular/common", "@ionic/angular/standalone"]
}

View File

@@ -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

View File

@@ -1,2 +0,0 @@
/build/*
!/build/.npmkeep

View File

@@ -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")
}

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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());
}
}

View File

@@ -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>

View File

@@ -1,5 +0,0 @@
package io.ionic.starter;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -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>

View File

@@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -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>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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
}

View File

@@ -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')

View File

@@ -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

View File

@@ -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

View File

@@ -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" "$@"

View File

@@ -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

View File

@@ -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'

View File

@@ -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'
}

View File

@@ -1,158 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"app": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "www",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "src/assets",
"output": "assets"
},
{
"glob": "**/*.svg",
"input": "node_modules/ionicons/dist/ionicons/svg",
"output": "./svg"
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/global.scss",
"src/theme/variables.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
},
"ci": {
"progress": false
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "app:build:production"
},
"development": {
"buildTarget": "app:build:development"
},
"ci": {
"progress": false
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "app:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "src/assets",
"output": "assets"
},
{
"glob": "**/*.svg",
"input": "node_modules/ionicons/dist/ionicons/svg",
"output": "./svg"
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/global.scss",
"src/theme/variables.scss"
],
"scripts": []
},
"configurations": {
"ci": {
"progress": false,
"watch": false
}
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}
}
},
"cli": {
"schematicCollections": [
"@ionic/angular-toolkit"
],
"analytics": false
},
"schematics": {
"@ionic/angular-toolkit:component": {
"styleext": "scss"
},
"@ionic/angular-toolkit:page": {
"styleext": "scss"
}
}
}

View File

@@ -1,9 +0,0 @@
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'io.ionic.starter',
appName: 'allowance-planner-v2',
webDir: 'www'
};
export default config;

View File

@@ -1,7 +0,0 @@
{
"name": "allowance-planner-v2",
"integrations": {
"capacitor": {}
},
"type": "angular"
}

View File

@@ -1,44 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/app'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,70 +0,0 @@
{
"name": "allowance-planner-v2",
"version": "0.0.1",
"author": "Ionic Framework",
"homepage": "https://ionicframework.com/",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"lint": "ng lint"
},
"private": true,
"dependencies": {
"@angular/animations": "^19.0.0",
"@angular/cdk": "^19.2.15",
"@angular/common": "^19.0.0",
"@angular/compiler": "^19.0.0",
"@angular/core": "^19.0.0",
"@angular/forms": "^19.0.0",
"@angular/material": "^19.2.15",
"@angular/platform-browser": "^19.0.0",
"@angular/platform-browser-dynamic": "^19.0.0",
"@angular/router": "^19.0.0",
"@capacitor/android": "7.2.0",
"@capacitor/app": "7.0.1",
"@capacitor/core": "7.2.0",
"@capacitor/haptics": "7.0.1",
"@capacitor/keyboard": "7.0.1",
"@capacitor/status-bar": "7.0.1",
"@ionic/angular": "^8.0.0",
"@ionic/pwa-elements": "^3.3.0",
"@ionic/storage-angular": "^4.0.0",
"ionicons": "^7.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^19.0.0",
"@angular-eslint/builder": "^19.0.0",
"@angular-eslint/eslint-plugin": "^19.0.0",
"@angular-eslint/eslint-plugin-template": "^19.0.0",
"@angular-eslint/schematics": "^19.0.0",
"@angular-eslint/template-parser": "^19.0.0",
"@angular/cli": "^19.0.0",
"@angular/compiler-cli": "^19.0.0",
"@angular/language-service": "^19.0.0",
"@capacitor/assets": "^3.0.5",
"@capacitor/cli": "7.2.0",
"@ionic/angular-toolkit": "^12.0.0",
"@types/jasmine": "~5.1.0",
"@typescript-eslint/eslint-plugin": "^8.18.0",
"@typescript-eslint/parser": "^8.18.0",
"eslint": "^9.16.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsdoc": "^48.2.1",
"eslint-plugin-prefer-arrow": "1.2.2",
"jasmine-core": "~5.1.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.6.3"
},
"description": "An Ionic project"
}

View File

@@ -1,22 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: '',
loadChildren: () => import('./pages/user-login/user-login.module').then( m => m.UserLoginPageModule)
},
{
path: '',
loadChildren: () => import('./pages/tabs/tabs.module').then(m => m.TabsPageModule)
},
];
@NgModule({
imports: [
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }),
CommonModule
],
exports: [RouterModule]
})
export class AppRoutingModule {}

View File

@@ -1,3 +0,0 @@
<ion-app>
<ion-router-outlet></ion-router-outlet>
</ion-app>

View File

@@ -1,21 +0,0 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
});

View File

@@ -1,21 +0,0 @@
import { Component } from '@angular/core';
import { StorageService } from './services/storage.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
standalone: false,
})
export class AppComponent {
constructor(private storageService: StorageService, private router: Router) {
this.storageService.init().then(() => {
this.storageService.getCurrentUserId().then((userId) => {
if (userId !== undefined && userId !== null) {
this.router.navigate(['/tabs/allowance', userId]);
}
});
})
}
}

View File

@@ -1,28 +0,0 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { Drivers } from '@ionic/storage';
import { IonicStorageModule } from '@ionic/storage-angular';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
IonicModule.forRoot(),
AppRoutingModule,
ReactiveFormsModule,
IonicStorageModule.forRoot({
name: '__mydb',
driverOrder: [Drivers.IndexedDB, Drivers.LocalStorage]
})
],
providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
bootstrap: [AppComponent],
})
export class AppModule {}

View File

@@ -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;
}

View File

@@ -1,5 +0,0 @@
export interface History {
timestamp: string;
allowance: number;
description: string;
}

View File

@@ -1,6 +0,0 @@
export interface Task {
id: number;
name: string;
reward: number;
assigned: number | null;
}

View File

@@ -1,5 +0,0 @@
export interface User {
id: number;
name: string;
allowance?: number
}

View File

@@ -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 {}

View File

@@ -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 {}

View File

@@ -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>

View File

@@ -1,40 +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;
}
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: auto;
margin-bottom: 50px;
}
button:disabled,
button[disabled]{
opacity: 0.5;
}

Some files were not shown because too many files have changed in this diff Show More