2 Commits

79 changed files with 168 additions and 20414 deletions

View File

@@ -1,9 +1,2 @@
# Allowance Planner 2000
An improved Allowance Planner app.
## Running backend
In order to run the backend, go to the `backend directory and run:
```bash
$ go run .
```
An improved Allowance Planner app.

View File

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

View File

@@ -67,53 +67,27 @@ func (db *Db) UserExists(userId int) (bool, error) {
return count > 0, nil
}
func (db *Db) GetUserAllowances(userId int) ([]Allowance, error) {
allowances := make([]Allowance, 0)
func (db *Db) GetUserGoals(userId int) ([]Goal, error) {
goals := make([]Goal, 0)
var err error
totalAllowance := Allowance{}
err = db.db.Query("select balance, weight from users where id = ?").Bind(userId).ScanSingle(&totalAllowance.Progress, &totalAllowance.Weight)
if err != nil {
return nil, err
}
allowances = append(allowances, totalAllowance)
for row := range db.db.Query("select id, name, target, balance, weight from allowances where user_id = ?").
for row := range db.db.Query("select id, name, target, progress, weight from goals where user_id = ?").
Bind(userId).Range(&err) {
allowance := Allowance{}
err = row.Scan(&allowance.ID, &allowance.Name, &allowance.Target, &allowance.Progress, &allowance.Weight)
goal := Goal{}
err = row.Scan(&goal.ID, &goal.Name, &goal.Target, &goal.Progress, &goal.Weight)
if err != nil {
return nil, err
}
allowances = append(allowances, allowance)
goals = append(goals, goal)
}
if err != nil {
return nil, err
}
return allowances, nil
return goals, nil
}
func (db *Db) GetUserAllowanceById(userId int, allowanceId int) (*Allowance, error) {
allowance := &Allowance{}
if allowanceId == 0 {
err := db.db.Query("select balance, weight from users where id = ?").
Bind(userId).ScanSingle(&allowance.Progress, &allowance.Weight)
if err != nil {
return nil, err
}
} else {
err := db.db.Query("select id, name, target, balance, weight from allowances where user_id = ? and id = ?").
Bind(userId, allowanceId).
ScanSingle(&allowance.ID, &allowance.Name, &allowance.Target, &allowance.Progress, &allowance.Weight)
if err != nil {
return nil, err
}
}
return allowance, nil
}
func (db *Db) CreateAllowance(userId int, allowance *CreateAllowanceRequest) (int, error) {
// Check if user exists before attempting to create an allowance
func (db *Db) CreateGoal(userId int, goal *CreateGoalRequest) (int, error) {
// Check if user exists before attempting to create a goal
exists, err := db.UserExists(userId)
if err != nil {
return 0, err
@@ -128,9 +102,9 @@ func (db *Db) CreateAllowance(userId int, allowance *CreateAllowanceRequest) (in
}
defer tx.MustRollback()
// Insert the new allowance
err = tx.Query("insert into allowances (user_id, name, target, weight) values (?, ?, ?, ?)").
Bind(userId, allowance.Name, allowance.Target, allowance.Weight).
// Insert the new goal
err = tx.Query("insert into goals (user_id, name, target, progress, weight) values (?, ?, ?, 0, ?)").
Bind(userId, goal.Name, goal.Target, goal.Weight).
Exec()
if err != nil {
@@ -153,21 +127,21 @@ func (db *Db) CreateAllowance(userId int, allowance *CreateAllowanceRequest) (in
return lastId, nil
}
func (db *Db) DeleteAllowance(userId int, allowanceId int) error {
// Check if the allowance exists for the user
func (db *Db) DeleteGoal(userId int, goalId int) error {
// Check if the goal exists for the user
count := 0
err := db.db.Query("select count(*) from allowances where id = ? and user_id = ?").
Bind(allowanceId, userId).ScanSingle(&count)
err := db.db.Query("select count(*) from goals where id = ? and user_id = ?").
Bind(goalId, userId).ScanSingle(&count)
if err != nil {
return err
}
if count == 0 {
return errors.New("allowance not found")
return errors.New("goal not found")
}
// Delete the allowance
err = db.db.Query("delete from allowances where id = ? and user_id = ?").
Bind(allowanceId, userId).Exec()
// Delete the goal
err = db.db.Query("delete from goals where id = ? and user_id = ?").
Bind(goalId, userId).Exec()
if err != nil {
return err
}
@@ -175,107 +149,6 @@ func (db *Db) DeleteAllowance(userId int, allowanceId int) error {
return nil
}
func (db *Db) CompleteAllowance(userId int, allowanceId int) error {
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
// Get the cost of the allowance
var cost int
err = tx.Query("select balance from allowances where id = ? and user_id = ?").
Bind(allowanceId, userId).ScanSingle(&cost)
if err != nil {
return err
}
// Delete the allowance
err = tx.Query("delete from allowances where id = ? and user_id = ?").
Bind(allowanceId, userId).Exec()
if err != nil {
return err
}
// Add a history entry
err = tx.Query("insert into history (user_id, timestamp, amount) values (?, ?, ?)").
Bind(userId, time.Now().Unix(), -cost).
Exec()
if err != nil {
return err
}
return tx.Commit()
}
func (db *Db) UpdateUserAllowance(userId int, allowance *UpdateAllowanceRequest) error {
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
err = tx.Query("update users set weight=? where id = ?").
Bind(allowance.Weight, userId).
Exec()
if err != nil {
return err
}
return tx.Commit()
}
func (db *Db) UpdateAllowance(userId int, allowanceId int, allowance *UpdateAllowanceRequest) error {
// Check if the allowance exists for the user
count := 0
err := db.db.Query("select count(*) from allowances where id = ? and user_id = ?").
Bind(allowanceId, userId).ScanSingle(&count)
if err != nil {
return err
}
if count == 0 {
return errors.New("allowance not found")
}
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
err = tx.Query("update allowances set name=?, target=?, weight=? where id = ? and user_id = ?").
Bind(allowance.Name, allowance.Target, allowance.Weight, allowanceId, userId).
Exec()
if err != nil {
return err
}
return tx.Commit()
}
func (db *Db) BulkUpdateAllowance(userId int, allowances []BulkUpdateAllowanceRequest) error {
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
for _, allowance := range allowances {
if allowance.ID == 0 {
err = tx.Query("update users set weight=? where id = ?").
Bind(allowance.Weight, userId).
Exec()
} else {
err = tx.Query("update allowances set weight=? where id = ? and user_id = ?").
Bind(allowance.Weight, allowance.ID, userId).
Exec()
}
if err != nil {
return err
}
}
return tx.Commit()
}
func (db *Db) CreateTask(task *CreateTaskRequest) (int, error) {
tx, err := db.db.Begin()
if err != nil {
@@ -337,21 +210,6 @@ func (db *Db) GetTask(id int) (Task, error) {
return task, nil
}
func (db *Db) DeleteTask(id int) error {
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
err = tx.Query("delete from tasks where id = ?").Bind(id).Exec()
if err != nil {
return err
}
return tx.Commit()
}
func (db *Db) HasTask(id int) (bool, error) {
count := 0
err := db.db.Query("select count(*) from tasks where id = ?").
@@ -378,82 +236,7 @@ func (db *Db) UpdateTask(id int, task *CreateTaskRequest) error {
return tx.Commit()
}
func (db *Db) CompleteTask(taskId int) error {
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
var reward int
err = tx.Query("select reward from tasks where id = ?").Bind(taskId).ScanSingle(&reward)
if err != nil {
return err
}
for userRow := range tx.Query("select id, weight from users").Range(&err) {
var userId int
var userWeight float64
err = userRow.Scan(&userId, &userWeight)
if err != nil {
return err
}
// Add the history entry
err = tx.Query("insert into history (user_id, timestamp, amount) values (?, ?, ?)").
Bind(userId, time.Now().Unix(), reward).
Exec()
if err != nil {
return err
}
// Calculate the sums of all weights
var sumOfWeights float64
err = tx.Query("select sum(weight) from allowances where user_id = ? and weight > 0").Bind(userId).ScanSingle(&sumOfWeights)
sumOfWeights += userWeight
remainingReward := reward
if sumOfWeights > 0 {
// Distribute the reward to the allowances
for allowanceRow := range tx.Query("select id, weight from allowances where user_id = ? and weight > 0").Bind(userId).Range(&err) {
var allowanceId int
var allowanceWeight float64
err = allowanceRow.Scan(&allowanceId, &allowanceWeight)
if err != nil {
return err
}
// Calculate the amount to add to the allowance
amount := int((allowanceWeight / sumOfWeights) * float64(remainingReward))
sumOfWeights -= allowanceWeight
err = tx.Query("update allowances set balance = balance + ? where id = ? and user_id = ?").
Bind(amount, allowanceId, userId).Exec()
if err != nil {
return err
}
remainingReward -= amount
}
}
// Add the remaining reward to the user
err = tx.Query("update users set balance = balance + ? where id = ?").
Bind(remainingReward, userId).Exec()
if err != nil {
return err
}
}
if err != nil {
return err
}
// Remove the task
err = tx.Query("delete from tasks where id = ?").Bind(taskId).Exec()
return tx.Commit()
}
func (db *Db) AddHistory(userId int, allowance *PostHistory) error {
func (db *Db) AddAllowance(userId int, allowance *PostAllowance) error {
tx, err := db.db.Begin()
if err != nil {
return err
@@ -469,19 +252,17 @@ func (db *Db) AddHistory(userId int, allowance *PostHistory) error {
return tx.Commit()
}
func (db *Db) GetHistory(userId int) ([]History, error) {
history := make([]History, 0)
func (db *Db) GetHistory(userId int) ([]Allowance, error) {
history := make([]Allowance, 0)
var err error
for row := range db.db.Query("select amount, `timestamp` 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) {
allowance := History{}
var timestamp int64
err = row.Scan(&allowance.Allowance, &timestamp)
allowance := Allowance{}
err = row.Scan(&allowance.Allowance)
if err != nil {
return nil, err
}
allowance.Timestamp = time.Unix(timestamp, 0)
history = append(history, allowance)
}
if err != nil {

View File

@@ -13,12 +13,12 @@ type UserWithAllowance struct {
Allowance int `json:"allowance"`
}
type History struct {
Allowance int `json:"allowance"`
Timestamp time.Time `json:"timestamp"`
type Allowance struct {
Allowance int `json:"allowance"`
Timestamp time.Time
}
type PostHistory struct {
type PostAllowance struct {
Allowance int `json:"allowance"`
}
@@ -30,29 +30,18 @@ type Task struct {
Assigned *int `json:"assigned"` // Pointer to allow null
}
type Allowance struct {
ID int `json:"id"`
Name string `json:"name"`
Target int `json:"target"`
Progress int `json:"progress"`
Weight float64 `json:"weight"`
type Goal struct {
ID int `json:"id"`
Name string `json:"name"`
Target int `json:"target"`
Progress int `json:"progress"`
Weight int `json:"weight"`
}
type CreateAllowanceRequest struct {
Name string `json:"name"`
Target int `json:"target"`
Weight float64 `json:"weight"`
}
type UpdateAllowanceRequest struct {
Name string `json:"name"`
Target int `json:"target"`
Weight float64 `json:"weight"`
}
type BulkUpdateAllowanceRequest struct {
ID int `json:"id"`
Weight float64 `json:"weight"`
type CreateGoalRequest struct {
Name string `json:"name"`
Target int `json:"target"`
Weight int `json:"weight"`
}
type CreateGoalResponse struct {

View File

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

View File

@@ -1,9 +1,5 @@
gitea.seeseepuff.be/seeseemelk/mysqlite v0.12.0 h1:kl0VFgvm52UKxJhZpf1hvucxZdOoXY50g/VmzsWH+/8=
gitea.seeseepuff.be/seeseemelk/mysqlite v0.12.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
gitea.seeseepuff.be/seeseemelk/mysqlite v0.13.0 h1:nqSXu5i5fHB1rrx/kfi8Phn/J6eFa2yh02FiGc9U1yg=
gitea.seeseepuff.be/seeseemelk/mysqlite v0.13.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0 h1:aRItVfUj48fBmuec7rm/jY9KCfvHW2VzJfItVk4t8sw=
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
gitea.seeseepuff.be/seeseemelk/mysqlite v0.11.1 h1:5s0r2IRpomGJC6pjirdMk7HAcAYEydLK5AhBZy+V1Ys=
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/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
@@ -32,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/gavv/httpexpect/v2 v2.17.0 h1:nIJqt5v5e4P7/0jODpX2gtSw+pHXUqdP28YcjqwDZmE=
github.com/gavv/httpexpect/v2 v2.17.0/go.mod h1:E8ENFlT9MZ3Si2sfM6c6ONdwXV2noBCGkhA+lkJgkP0=
github.com/gin-contrib/cors v1.7.5 h1:cXC9SmofOrRg0w9PigwGlHG3ztswH6bqq4vJVXnvYMk=
github.com/gin-contrib/cors v1.7.5/go.mod h1:4q3yi7xBEDDWKapjT2o1V7mScKDDr8k+jZ0fSquGoy0=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
@@ -74,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/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
@@ -107,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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg=
github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
@@ -197,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/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
@@ -212,22 +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=
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
modernc.org/ccgo/v4 v4.27.1 h1:emhLB4uoOmkZUnTDFcMI3AbkmU/Evjuerit9Taqe6Ss=
modernc.org/ccgo/v4 v4.27.1/go.mod h1:543Q0qQhJWekKVS5P6yL5fO6liNhla9Lbm2/B3rEKDE=
modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8=
modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.65.6 h1:OhJUhmuJ6MVZdqL5qmnd0/my46DKGFhSX4WOR7ijfyE=
modernc.org/libc v1.65.6/go.mod h1:MOiGAM9lrMBT9L8xT1nO41qYl5eg9gCp9/kWhz5L7WA=
modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
modernc.org/libc v1.65.2 h1:drWL1QO9fKXr3kXDN8y+4lKyBr8bA3mtUBQpftq3IJw=
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/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=

View File

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

View File

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

View File

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

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,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,68 +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/app": "7.0.1",
"@capacitor/core": "7.2.0",
"@capacitor/haptics": "7.0.1",
"@capacitor/keyboard": "7.0.1",
"@capacitor/status-bar": "7.0.1",
"@ionic/angular": "^8.0.0",
"@ionic/pwa-elements": "^3.3.0",
"@ionic/storage-angular": "^4.0.0",
"ionicons": "^7.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^19.0.0",
"@angular-eslint/builder": "^19.0.0",
"@angular-eslint/eslint-plugin": "^19.0.0",
"@angular-eslint/eslint-plugin-template": "^19.0.0",
"@angular-eslint/schematics": "^19.0.0",
"@angular-eslint/template-parser": "^19.0.0",
"@angular/cli": "^19.0.0",
"@angular/compiler-cli": "^19.0.0",
"@angular/language-service": "^19.0.0",
"@capacitor/cli": "7.2.0",
"@ionic/angular-toolkit": "^12.0.0",
"@types/jasmine": "~5.1.0",
"@typescript-eslint/eslint-plugin": "^8.18.0",
"@typescript-eslint/parser": "^8.18.0",
"eslint": "^9.16.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsdoc": "^48.2.1",
"eslint-plugin-prefer-arrow": "1.2.2",
"jasmine-core": "~5.1.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.6.3"
},
"description": "An Ionic project"
}

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,26 +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, Storage } from '@ionic/storage';
import { IonicStorageModule } from '@ionic/storage-angular';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
IonicModule.forRoot(),
AppRoutingModule,
IonicStorageModule.forRoot({
name: '__mydb',
driverOrder: [Drivers.IndexedDB, Drivers.LocalStorage]
})
],
providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
bootstrap: [AppComponent],
})
export class AppModule {}

View File

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

View File

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

View File

@@ -1,16 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AllowancePage } from './allowance.page';
const routes: Routes = [
{
path: ':id',
component: AllowancePage,
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AllowancePageRoutingModule {}

View File

@@ -1,18 +0,0 @@
import { IonicModule } from '@ionic/angular';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { AllowancePage } from './allowance.page';
import { AllowancePageRoutingModule } from './allowance-routing.module';
@NgModule({
imports: [
IonicModule,
CommonModule,
FormsModule,
AllowancePageRoutingModule
],
declarations: [AllowancePage]
})
export class AllowancePageModule {}

View File

@@ -1,10 +0,0 @@
<ion-header [translucent]="true" class="ion-no-border">
<ion-toolbar>
<ion-title>
Allowance
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
</ion-content>

View File

@@ -1,26 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
import { AllowancePage } from './allowance.page';
describe('AllowancePage', () => {
let component: AllowancePage;
let fixture: ComponentFixture<AllowancePage>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AllowancePage],
imports: [IonicModule.forRoot(), ExploreContainerComponentModule]
}).compileComponents();
fixture = TestBed.createComponent(AllowancePage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,14 +0,0 @@
import { Component } from '@angular/core';
import { UserService } from 'src/app/services/user.service';
@Component({
selector: 'app-allowance',
templateUrl: 'allowance.page.html',
styleUrls: ['allowance.page.scss'],
standalone: false,
})
export class AllowancePage {
constructor(private userService: UserService) {}
}

View File

@@ -1,16 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HistoryPage } from './history.page';
const routes: Routes = [
{
path: '',
component: HistoryPage,
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class HistoryPageRoutingModule {}

View File

@@ -1,18 +0,0 @@
import { IonicModule } from '@ionic/angular';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { HistoryPage } from './history.page';
import { HistoryPageRoutingModule } from './history-routing.module';
@NgModule({
imports: [
IonicModule,
CommonModule,
FormsModule,
HistoryPageRoutingModule
],
declarations: [HistoryPage]
})
export class HistoryPageModule {}

View File

@@ -1,11 +0,0 @@
<ion-header [translucent]="true" class="ion-no-border">
<ion-toolbar>
<ion-title>
History
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
</ion-content>

View File

@@ -1,26 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
import { HistoryPage } from './history.page';
describe('HistoryPage', () => {
let component: HistoryPage;
let fixture: ComponentFixture<HistoryPage>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [HistoryPage],
imports: [IonicModule.forRoot(), ExploreContainerComponentModule]
}).compileComponents();
fixture = TestBed.createComponent(HistoryPage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,13 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-history',
templateUrl: 'history.page.html',
styleUrls: ['history.page.scss'],
standalone: false,
})
export class HistoryPage {
constructor() {}
}

View File

@@ -1,39 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { TabsPage } from './tabs.page';
const routes: Routes = [
{
path: 'tabs',
component: TabsPage,
children: [
{
path: 'history',
loadChildren: () => import('../history/history.module').then(m => m.HistoryPageModule)
},
{
path: 'allowance',
loadChildren: () => import('../allowance/allowance.module').then(m => m.AllowancePageModule)
},
{
path: 'tasks',
loadChildren: () => import('../tasks/tasks.module').then(m => m.TasksPageModule)
},
{
path: '',
redirectTo: '/tabs/allowance',
pathMatch: 'full'
}
]
},
{
path: '',
redirectTo: '/tabs/allowance',
pathMatch: 'full'
}
];
@NgModule({
imports: [RouterModule.forChild(routes),],
})
export class TabsPageRoutingModule {}

View File

@@ -1,27 +0,0 @@
import { IonicModule } from '@ionic/angular';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import {MatIconModule} from '@angular/material/icon';
import { TabsPageRoutingModule } from './tabs-routing.module';
import { TabsPage } from './tabs.page';
import { provideHttpClient } from '@angular/common/http';
import { UserService } from 'src/app/services/user.service';
@NgModule({
imports: [
IonicModule,
CommonModule,
FormsModule,
TabsPageRoutingModule,
MatIconModule,
],
declarations: [TabsPage],
providers: [
provideHttpClient(),
UserService
]
})
export class TabsPageModule {}

View File

@@ -1,13 +0,0 @@
<ion-tabs>
<ion-tab-bar slot="bottom">
<ion-tab-button tab="history" href="/tabs/history">
<mat-icon>history</mat-icon>
</ion-tab-button>
<ion-tab-button tab="allowance" href="/tabs/allowance">
<mat-icon>savings</mat-icon>
</ion-tab-button>
<ion-tab-button tab="tasks" href="/tabs/tasks">
<mat-icon>task_alt</mat-icon>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>

View File

@@ -1,3 +0,0 @@
.tab-selected {
background-color: var(--ion-color-secondary);
}

View File

@@ -1,26 +0,0 @@
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TabsPage } from './tabs.page';
describe('TabsPage', () => {
let component: TabsPage;
let fixture: ComponentFixture<TabsPage>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TabsPage],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TabsPage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,12 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-tabs',
templateUrl: 'tabs.page.html',
styleUrls: ['tabs.page.scss'],
standalone: false,
})
export class TabsPage {
constructor() {}
}

View File

@@ -1,16 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { TasksPage } from './tasks.page';
const routes: Routes = [
{
path: '',
component: TasksPage,
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class TasksPageRoutingModule {}

View File

@@ -1,26 +0,0 @@
import { IonicModule } from '@ionic/angular';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { TasksPage } from './tasks.page';
import { TasksPageRoutingModule } from './tasks-routing.module';
import { provideHttpClient } from '@angular/common/http';
import { TaskService } from 'src/app/services/task.service';
import { MatIconModule } from '@angular/material/icon';
@NgModule({
imports: [
IonicModule,
CommonModule,
FormsModule,
TasksPageRoutingModule,
MatIconModule,
],
declarations: [TasksPage],
providers: [
provideHttpClient(),
TaskService
]
})
export class TasksPageModule {}

View File

@@ -1,23 +0,0 @@
<ion-header [translucent]="true" class="ion-no-border">
<ion-toolbar>
<ion-title>
Tasks
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="icon">
<mat-icon>filter_alt</mat-icon>
</div>
<div class="list">
<div class="task" *ngFor="let task of tasks">
<button>Done</button>
<div class="name">{{ task.name }}</div>
<div
class="reward"
[ngClass]="{ 'negative': task.reward < 0 }"
>{{ task.reward.toFixed(2) }} SP</div>
</div>
</div>
</ion-content>

View File

@@ -1,47 +0,0 @@
.icon {
padding: 5px;
display: flex;
justify-content: flex-end;
color: var(--ion-color-primary);
}
mat-icon {
font-size: 35px;
width: 35px;
height: 35px;
}
.list {
border-top: 1px solid var(--line-color);
}
.task {
display: flex;
flex-direction: row;
align-items: center;
border-bottom: 1px solid var(--line-color);
padding: 5px;
}
.name {
margin-left: 10px;
color: var(--font-color);
}
.reward {
margin-left: auto;
margin-right: 15px;
color: var(--positive-amount-color);
}
.negative {
color: var(--negative-amount-color);
}
button {
width: 57px;
height: 30px;
border-radius: 10px;
color: white;
background: var(--confirm-button-color);
}

View File

@@ -1,26 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
import { TasksPage } from './tasks.page';
describe('TasksPage', () => {
let component: TasksPage;
let fixture: ComponentFixture<TasksPage>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TasksPage],
imports: [IonicModule.forRoot(), ExploreContainerComponentModule]
}).compileComponents();
fixture = TestBed.createComponent(TasksPage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,24 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { TaskService } from 'src/app/services/task.service';
import { Task } from 'src/app/models/task';
@Component({
selector: 'app-tasks',
templateUrl: 'tasks.page.html',
styleUrls: ['tasks.page.scss'],
standalone: false,
})
export class TasksPage implements OnInit {
public tasks: Array<Task> = [];
constructor(
private taskService: TaskService
) {}
ngOnInit(): void {
this.taskService.getTaskList().subscribe(tasks => {
this.tasks = tasks;
});
}
}

View File

@@ -1,17 +0,0 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { UserLoginPage } from './user-login.page';
const routes: Routes = [
{
path: '',
component: UserLoginPage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class UserLoginPageRoutingModule {}

View File

@@ -1,26 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { UserLoginPageRoutingModule } from './user-login-routing.module';
import { UserLoginPage } from './user-login.page';
import { provideHttpClient } from '@angular/common/http';
import { UserService } from 'src/app/services/user.service';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
UserLoginPageRoutingModule,
],
declarations: [UserLoginPage],
providers: [
provideHttpClient(),
UserService
]
})
export class UserLoginPageModule {}

View File

@@ -1,9 +0,0 @@
<ion-content>
<div class="title">Who are you?</div>
<div class="selection">
<div class="profile" *ngFor="let user of users">
<div class="picture" (click)="selectUser(user.id)"></div>
<div class="name">{{ user.name }}</div>
</div>
</div>
</ion-content>

View File

@@ -1,31 +0,0 @@
.title,
.selection,
.profile {
display: flex;
justify-content: center;
}
.title {
margin-top: 150px;
color: var(--ion-color-primary);
font-size: 40px;
}
.selection {
gap: 10%;
margin-top: 100px;
color: var(--ion-color-primary);
}
.profile {
flex-direction: column;
align-items: center;
}
.picture {
width: 130px;
height: 130px;
border: 1px solid var(--ion-color-primary);
border-radius: 10px;
background-color: var(--ion-color-secondary);
}

View File

@@ -1,17 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserLoginPage } from './user-login.page';
describe('UserLoginPage', () => {
let component: UserLoginPage;
let fixture: ComponentFixture<UserLoginPage>;
beforeEach(() => {
fixture = TestBed.createComponent(UserLoginPage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,32 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { User } from 'src/app/models/user';
import { StorageService } from 'src/app/services/storage.service';
import { UserService } from 'src/app/services/user.service';
@Component({
selector: 'app-user-login',
templateUrl: './user-login.page.html',
styleUrls: ['./user-login.page.scss'],
standalone: false,
})
export class UserLoginPage implements OnInit {
public users: Array<User> = [];
constructor(
private userService: UserService,
private storageService: StorageService,
private router: Router
) { }
ngOnInit() {
this.userService.getUserList().subscribe(users => {
this.users = users
});
}
selectUser(id: number) {
this.storageService.set('user-id', id);
this.router.navigate(['/tabs/allowance', id]);
}
}

View File

@@ -1,28 +0,0 @@
import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage-angular';
@Injectable({
providedIn: 'root'
})
export class StorageService {
private _storage: Storage | null = null;
constructor(private storage: Storage) {}
async init() {
// If using, define drivers here: await this.storage.defineDriver(/*...*/);
const storage = await this.storage.create();
this._storage = storage;
}
// Create and expose methods that users of this service can
// call, for example:
public set(key: string, value: any) {
this._storage?.set(key, value);
}
public async getCurrentUserId() {
return await this._storage?.get('user-id');
}
}

View File

@@ -1,16 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Task } from '../models/task';
@Injectable({
providedIn: 'root'
})
export class TaskService {
private url = 'http://localhost:8080/api'
constructor(private http: HttpClient) {}
getTaskList(): Observable<Array<Task>> {
return this.http.get<Task[]>(`${this.url}/tasks`);
}
}

View File

@@ -1,20 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { User } from '../models/user';
@Injectable({
providedIn: 'root',
})
export class UserService {
private url = 'http://localhost:8080/api';
constructor(private http: HttpClient) {}
getUserList(): Observable<Array<User>> {
return this.http.get<User[]>(`${this.url}/users`);
}
getUserById(id: number): Observable<User> {
return this.http.get<User>(`${this.url}/user/${id}`);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 930 B

View File

@@ -1 +0,0 @@
<svg width="350" height="140" xmlns="http://www.w3.org/2000/svg" style="background:#f6f7f9"><g fill="none" fill-rule="evenodd"><path fill="#F04141" style="mix-blend-mode:multiply" d="M61.905-34.23l96.194 54.51-66.982 54.512L22 34.887z"/><circle fill="#10DC60" style="mix-blend-mode:multiply" cx="155.5" cy="135.5" r="57.5"/><path fill="#3880FF" style="mix-blend-mode:multiply" d="M208.538 9.513l84.417 15.392L223.93 93.93z"/><path fill="#FFCE00" style="mix-blend-mode:multiply" d="M268.625 106.557l46.332-26.75 46.332 26.75v53.5l-46.332 26.75-46.332-26.75z"/><circle fill="#7044FF" style="mix-blend-mode:multiply" cx="299.5" cy="9.5" r="38.5"/><rect fill="#11D3EA" style="mix-blend-mode:multiply" transform="rotate(-60 148.47 37.886)" x="143.372" y="-7.056" width="10.196" height="89.884" rx="5.098"/><path d="M-25.389 74.253l84.86 8.107c5.498.525 9.53 5.407 9.004 10.905a10 10 0 0 1-.057.477l-12.36 85.671a10.002 10.002 0 0 1-11.634 8.42l-86.351-15.226c-5.44-.959-9.07-6.145-8.112-11.584l13.851-78.551a10 10 0 0 1 10.799-8.219z" fill="#7044FF" style="mix-blend-mode:multiply"/><circle fill="#0CD1E8" style="mix-blend-mode:multiply" cx="273.5" cy="106.5" r="20.5"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,3 +0,0 @@
export const environment = {
production: true
};

View File

@@ -1,16 +0,0 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.

View File

@@ -1,45 +0,0 @@
/*
* App Global CSS
* ----------------------------------------------------------------------------
* Put style rules here that you want to apply globally. These styles are for
* the entire app and not just one component. Additionally, this file can be
* used as an entry point to import other CSS/Sass files to be included in the
* output CSS.
* For more information on global stylesheets, visit the documentation:
* https://ionicframework.com/docs/layout/global-stylesheets
*/
/* Core CSS required for Ionic components to work properly */
@import "@ionic/angular/css/core.css";
/* Basic CSS for apps built with Ionic */
@import "@ionic/angular/css/normalize.css";
@import "@ionic/angular/css/structure.css";
@import "@ionic/angular/css/typography.css";
@import "@ionic/angular/css/display.css";
/* Optional CSS utils that can be commented out */
@import "@ionic/angular/css/padding.css";
@import "@ionic/angular/css/float-elements.css";
@import "@ionic/angular/css/text-alignment.css";
@import "@ionic/angular/css/text-transformation.css";
@import "@ionic/angular/css/flex-utils.css";
/**
* Ionic Dark Mode
* -----------------------------------------------------
* For more info, please see:
* https://ionicframework.com/docs/theming/dark-mode
*/
/* @import "@ionic/angular/css/palettes/dark.always.css"; */
/* @import "@ionic/angular/css/palettes/dark.class.css"; */
@import "@ionic/angular/css/palettes/dark.system.css";
ion-title {
color: var(--ion-color-primary);
}
ion-header {
border-bottom: 1px solid var(--line-color);
}

View File

@@ -1,28 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Ionic App</title>
<base href="/" />
<meta name="color-scheme" content="light dark" />
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="format-detection" content="telephone=no" />
<meta name="msapplication-tap-highlight" content="no" />
<link rel="icon" type="image/png" href="assets/icon/favicon.png" />
<!-- add to homescreen for ios -->
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@@ -1,9 +0,0 @@
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { defineCustomElements } from '@ionic/pwa-elements/loader';
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));
// Call the element loader before the bootstrapModule/bootstrapApplication call
defineCustomElements(window);

View File

@@ -1,55 +0,0 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes recent versions of Safari, Chrome (including
* Opera), Edge on the desktop, and iOS and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
import './zone-flags';
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

View File

@@ -1,14 +0,0 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(),
);

View File

@@ -1,19 +0,0 @@
// For information on how to create your own theme, please see:
// http://ionicframework.com/docs/theming/
:root {
--ion-color-primary: #9C4BE4;
--ion-color-secondary: #F5E9FF;
--ion-background-color: #F3F3F3;
--font-color: #7B7B7B;
--confirm-button-color: #58A66F;
--positive-amount-color: #7DCB7D;
--negative-amount-color: #C55454;
--line-color: #CACACA;
--ion-font-family: 'Myfont';
}
@font-face {
font-family: 'MyFont';
src: url('../assets/font/Jaro-Regular.ttf');
}

View File

@@ -1,6 +0,0 @@
/**
* Prevents Angular change detection from
* running with certain Web Component callbacks
*/
// eslint-disable-next-line no-underscore-dangle
(window as any).__Zone_disable_customElements = true;

View File

@@ -1,15 +0,0 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.d.ts"
]
}

View File

@@ -1,33 +0,0 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "es2022",
"module": "es2020",
"lib": [
"es2018",
"dom"
],
"useDefineForClassFields": false
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

View File

@@ -1,18 +0,0 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"files": [
"src/test.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

View File

@@ -1,21 +0,0 @@
{
"name": "frontend",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@ionic/pwa-elements": "^3.3.0"
}
},
"node_modules/@ionic/pwa-elements": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@ionic/pwa-elements/-/pwa-elements-3.3.0.tgz",
"integrity": "sha512-vbykpxd2nGRlA67AnqDwsiVf8PUmInLyi6lQdnPDjeiML1WZa0CPe6r632nGDV9PTi+sWNde9Xexg9SD6Pwyqw==",
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
}
}
}
}

View File

@@ -1,5 +0,0 @@
{
"dependencies": {
"@ionic/pwa-elements": "^3.3.0"
}
}