Compare commits

..

4 Commits
main ... users

Author SHA1 Message Date
6d6460ac3e Add local storage 2025-05-14 18:30:13 +02:00
6979368eda Fix missing scheme in API url 2025-05-14 16:50:23 +02:00
fd14c12a4a Merge branch 'main' into users 2025-05-14 15:46:26 +02:00
df1b8e4ed7 setup usersa 2025-05-14 15:40:14 +02:00
51 changed files with 473 additions and 188 deletions

View File

@ -9,7 +9,7 @@ import (
) )
const ( const (
TestAllowanceName = "Test History" TestGoalName = "Test Goal"
) )
func startServer(t *testing.T) *httpexpect.Expect { func startServer(t *testing.T) *httpexpect.Expect {
@ -49,56 +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(0) result.Length().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": TestAllowanceName, "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(1) result.Length().IsEqual(1)
item := result.Value(0).Object() item := result.Value(0).Object()
item.Value("id").IsEqual(1) item.Value("id").IsEqual(1)
item.Value("name").IsEqual(TestAllowanceName) 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": TestAllowanceName, "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).
@ -106,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(1) goals.Length().IsEqual(1)
allowance := allowances.Value(0).Object() goal := goals.Value(0).Object()
allowance.Value("id").IsEqual(allowanceId) goal.Value("id").IsEqual(goalId)
allowance.Value("name").IsEqual(TestAllowanceName) 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": TestAllowanceName, "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
@ -149,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)
@ -159,76 +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": TestAllowanceName, "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": TestAllowanceName, "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(0) goals.Length().IsEqual(0)
} }
func TestDeleteUserAllowanceNotFound(t *testing.T) { func TestDeleteUserGoalNotFound(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) {
@ -323,7 +323,7 @@ func createTestTask(e *httpexpect.Expect) {
e.POST("/tasks").WithJSON(requestBody).Expect().Status(201) 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)
@ -391,9 +391,9 @@ func TestPutTaskInvalidTaskId(t *testing.T) {
func TestPostAllowance(t *testing.T) { func TestPostAllowance(t *testing.T) {
e := startServer(t) e := startServer(t)
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 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}).Expect().Status(200) e.POST("/user/1/allowance").WithJSON(PostAllowance{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: -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)
@ -402,16 +402,16 @@ func TestPostAllowance(t *testing.T) {
func TestPostAllowanceInvalidUserId(t *testing.T) { func TestPostAllowanceInvalidUserId(t *testing.T) {
e := startServer(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) Status(404)
} }
func TestGetHistory(t *testing.T) { func TestGetHistory(t *testing.T) {
e := startServer(t) e := startServer(t)
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 100}).Expect().Status(200) e.POST("/user/1/allowance").WithJSON(PostAllowance{Allowance: 100}).Expect().Status(200)
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 20}).Expect().Status(200) e.POST("/user/1/allowance").WithJSON(PostAllowance{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: -10}).Expect().Status(200)
response := e.GET("/user/1/history").Expect().Status(200).JSON().Array() response := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
response.Length().IsEqual(3) response.Length().IsEqual(3)

View File

@ -67,27 +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
for row := range db.db.Query("select id, name, target, progress, 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) { Bind(userId).Range(&err) {
allowance := Allowance{} goal := Goal{}
err = row.Scan(&allowance.ID, &allowance.Name, &allowance.Target, &allowance.Progress, &allowance.Weight) err = row.Scan(&goal.ID, &goal.Name, &goal.Target, &goal.Progress, &goal.Weight)
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) CreateAllowance(userId int, allowance *CreateAllowanceRequest) (int, error) { func (db *Db) CreateGoal(userId int, goal *CreateGoalRequest) (int, error) {
// Check if user exists before attempting to create an allowance // Check if user exists before attempting to create a goal
exists, err := db.UserExists(userId) exists, err := db.UserExists(userId)
if err != nil { if err != nil {
return 0, err return 0, err
@ -102,9 +102,9 @@ func (db *Db) CreateAllowance(userId int, allowance *CreateAllowanceRequest) (in
} }
defer tx.MustRollback() defer tx.MustRollback()
// Insert the new allowance // Insert the new goal
err = tx.Query("insert into allowances (user_id, name, target, progress, weight) values (?, ?, ?, 0, ?)"). err = tx.Query("insert into goals (user_id, name, target, progress, weight) values (?, ?, ?, 0, ?)").
Bind(userId, allowance.Name, allowance.Target, allowance.Weight). Bind(userId, goal.Name, goal.Target, goal.Weight).
Exec() Exec()
if err != nil { if err != nil {
@ -127,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
} }
@ -236,7 +236,7 @@ func (db *Db) UpdateTask(id int, task *CreateTaskRequest) error {
return tx.Commit() 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() tx, err := db.db.Begin()
if err != nil { if err != nil {
return err return err
@ -252,13 +252,13 @@ 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` from history where user_id = ? order by `timestamp` desc"). for row := range db.db.Query("select amount, `timestamp` from history where user_id = ? order by `timestamp` desc").
Bind(userId).Range(&err) { Bind(userId).Range(&err) {
allowance := History{} allowance := Allowance{}
var timestamp int64 var timestamp int64
err = row.Scan(&allowance.Allowance, &timestamp) err = row.Scan(&allowance.Allowance, &timestamp)
if err != nil { if err != nil {

View File

@ -13,12 +13,12 @@ type UserWithAllowance struct {
Allowance int `json:"allowance"` Allowance int `json:"allowance"`
} }
type History struct { type Allowance struct {
Allowance int `json:"allowance"` Allowance int `json:"allowance"`
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
} }
type PostHistory struct { type PostAllowance struct {
Allowance int `json:"allowance"` Allowance int `json:"allowance"`
} }
@ -30,7 +30,7 @@ type Task struct {
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 int `json:"target"` Target int `json:"target"`
@ -38,7 +38,7 @@ type Allowance struct {
Weight int `json:"weight"` Weight int `json:"weight"`
} }
type CreateAllowanceRequest struct { type CreateGoalRequest struct {
Name string `json:"name"` Name string `json:"name"`
Target int `json:"target"` Target int `json:"target"`
Weight int `json:"weight"` Weight int `json:"weight"`

View File

@ -76,7 +76,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,16 +97,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 createUserAllowance(c *gin.Context) { func createUserGoal(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 {
@ -116,7 +116,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"})
@ -125,12 +125,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" {
@ -146,9 +146,9 @@ func createUserAllowance(c *gin.Context) {
c.IndentedJSON(http.StatusCreated, response) c.IndentedJSON(http.StatusCreated, response)
} }
func deleteUserAllowance(c *gin.Context) { func deleteUserGoal(c *gin.Context) {
userIdStr := c.Param("userId") userIdStr := c.Param("userId")
allowanceIdStr := c.Param("allowanceId") goalIdStr := c.Param("goalId")
userId, err := strconv.Atoi(userIdStr) userId, err := strconv.Atoi(userIdStr)
if err != nil { if err != nil {
@ -157,10 +157,10 @@ func deleteUserAllowance(c *gin.Context) {
return return
} }
allowanceId, err := strconv.Atoi(allowanceIdStr) goalId, err := strconv.Atoi(goalIdStr)
if err != nil { if err != nil {
log.Printf("Invalid allowance ID: %v", err) log.Printf("Invalid goal ID: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid allowance ID"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid goal ID"})
return return
} }
@ -175,18 +175,18 @@ func deleteUserAllowance(c *gin.Context) {
return return
} }
err = db.DeleteAllowance(userId, allowanceId) err = db.DeleteGoal(userId, goalId)
if err != nil { if err != nil {
if err.Error() == "allowance not found" { if err.Error() == "goal not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "History not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Goal 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 createTask(c *gin.Context) { func createTask(c *gin.Context) {
@ -290,7 +290,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 postHistory(c *gin.Context) { func postAllowance(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 {
@ -299,8 +299,8 @@ 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
@ -317,13 +317,13 @@ 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 getHistory(c *gin.Context) {
@ -354,16 +354,14 @@ func start(ctx context.Context, config *ServerConfig) {
defer db.db.MustClose() defer db.db.MustClose()
router := gin.Default() router := gin.Default()
router.Use(cors.New(cors.Config{ router.Use(cors.Default())
AllowOrigins: []string{"*"},
}))
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/history", getHistory)
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.DELETE("/api/user/:userId/allowance/:allowanceId", deleteUserAllowance) router.DELETE("/api/user/:userId/goal/:goalId", deleteUserGoal)
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)

View File

@ -12,7 +12,7 @@ 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,

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

@ -25,6 +25,7 @@
"@capacitor/status-bar": "7.0.1", "@capacitor/status-bar": "7.0.1",
"@ionic/angular": "^8.0.0", "@ionic/angular": "^8.0.0",
"@ionic/pwa-elements": "^3.3.0", "@ionic/pwa-elements": "^3.3.0",
"@ionic/storage-angular": "^4.0.0",
"ionicons": "^7.0.0", "ionicons": "^7.0.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
@ -3779,6 +3780,27 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
} }
}, },
"node_modules/@ionic/storage": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@ionic/storage/-/storage-4.0.0.tgz",
"integrity": "sha512-3N21P19Xk6cICLnSXZ3ilRqbSXAGSFeIF3HNqz+1kARcm0UFT/vwmZreaXtFyq437vvEWOfJ2enlj3JHLKS0FA==",
"dependencies": {
"localforage": "^1.9.0"
}
},
"node_modules/@ionic/storage-angular": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@ionic/storage-angular/-/storage-angular-4.0.0.tgz",
"integrity": "sha512-FeSmCMCm1bMRfu5TFSqLtjdfEo/dLLUhLIrPmbhSYomVZdV/dNn4mBZv9SabyxSqn4bF31hw40y+4buhG+durQ==",
"dependencies": {
"@ionic/storage": "^4.0.0",
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/core": "*",
"rxjs": "*"
}
},
"node_modules/@ionic/utils-array": { "node_modules/@ionic/utils-array": {
"version": "2.1.6", "version": "2.1.6",
"resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz", "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz",
@ -10250,6 +10272,11 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"node_modules/immutable": { "node_modules/immutable": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
@ -11791,6 +11818,14 @@
} }
} }
}, },
"node_modules/lie": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
"integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lines-and-columns": { "node_modules/lines-and-columns": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@ -11893,6 +11928,14 @@
"node": ">= 12.13.0" "node": ">= 12.13.0"
} }
}, },
"node_modules/localforage": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
"dependencies": {
"lie": "3.1.1"
}
},
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",

View File

@ -30,6 +30,7 @@
"@capacitor/status-bar": "7.0.1", "@capacitor/status-bar": "7.0.1",
"@ionic/angular": "^8.0.0", "@ionic/angular": "^8.0.0",
"@ionic/pwa-elements": "^3.3.0", "@ionic/pwa-elements": "^3.3.0",
"@ionic/storage-angular": "^4.0.0",
"ionicons": "^7.0.0", "ionicons": "^7.0.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",

View File

@ -1,15 +0,0 @@
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title>
Allowance
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Allowance</ion-title>
</ion-toolbar>
</ion-header>
</ion-content>

View File

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

View File

@ -1,4 +1,6 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { StorageService } from './services/storage.service';
import { Router } from '@angular/router';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@ -7,5 +9,14 @@ import { Component } from '@angular/core';
standalone: false, standalone: false,
}) })
export class AppComponent { export class AppComponent {
constructor() {} constructor(private storageService: StorageService, private router: Router) {
this.storageService.init().then(() => {
this.storageService.getCurrentUserId().then((userId) => {
if (userId !== undefined && userId !== null) {
console.log('userId: ', userId);
this.router.navigate(['/tabs'], userId);
}
});
})
}
} }

View File

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

View File

@ -1,15 +0,0 @@
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title>
History
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">History</ion-title>
</ion-toolbar>
</ion-header>
</ion-content>

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { UserService } from 'src/app/services/user.service';
@Component({ @Component({
selector: 'app-allowance', selector: 'app-allowance',
@ -8,6 +9,6 @@ import { Component } from '@angular/core';
}) })
export class AllowancePage { export class AllowancePage {
constructor() {} constructor(private userService: UserService) {}
} }

View File

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

View File

@ -34,6 +34,6 @@ const routes: Routes = [
]; ];
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes),],
}) })
export class TabsPageRoutingModule {} export class TabsPageRoutingModule {}

View File

@ -7,6 +7,8 @@ import {MatIconModule} from '@angular/material/icon';
import { TabsPageRoutingModule } from './tabs-routing.module'; import { TabsPageRoutingModule } from './tabs-routing.module';
import { TabsPage } from './tabs.page'; import { TabsPage } from './tabs.page';
import { provideHttpClient } from '@angular/common/http';
import { UserService } from 'src/app/services/user.service';
@NgModule({ @NgModule({
imports: [ imports: [
@ -16,6 +18,10 @@ import { TabsPage } from './tabs.page';
TabsPageRoutingModule, TabsPageRoutingModule,
MatIconModule, MatIconModule,
], ],
declarations: [TabsPage] declarations: [TabsPage],
providers: [
provideHttpClient(),
UserService
]
}) })
export class TabsPageModule {} export class TabsPageModule {}

View File

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

View File

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

View File

@ -7,7 +7,6 @@ import { Component } from '@angular/core';
standalone: false, standalone: false,
}) })
export class TabsPage { export class TabsPage {
constructor() {} constructor() {}
} }

View File

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

View File

@ -0,0 +1,17 @@
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

@ -0,0 +1,26 @@
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

@ -0,0 +1,9 @@
<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

@ -0,0 +1,30 @@
.title,
.selection,
.profile {
display: flex;
justify-content: center;
}
.title {
margin-top: 40%;
color: var(--ion-color-primary);
font-size: 40px;
}
.selection {
gap: 10%;
margin-top: 20%;
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;
}

View File

@ -0,0 +1,17 @@
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

@ -0,0 +1,26 @@
import { Component, OnInit } from '@angular/core';
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) { }
ngOnInit() {
this.userService.getUserList().subscribe(users => {
this.users = users
});
}
selectUser(id: number) {
this.storageService.set('user-id', id);
}
}

View File

@ -0,0 +1,28 @@
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

@ -0,0 +1,20 @@
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}`);
}
}

View File

@ -1,15 +0,0 @@
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title>
Tasks
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Tasks</ion-title>
</ion-toolbar>
</ion-header>
</ion-content>

View File

@ -1 +1,37 @@
/*
* 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";

View File

@ -1,2 +1,14 @@
// For information on how to create your own theme, please see: // For information on how to create your own theme, please see:
// http://ionicframework.com/docs/theming/ // http://ionicframework.com/docs/theming/
:root {
--ion-color-primary: #9C4BE4;
--ion-color-secondary: #F5E9FF;
--ion-background-color: #F3F3F3;
--ion-font-family: 'Myfont';
}
@font-face {
font-family: 'MyFont';
src: url('../assets/font/Jaro-Regular.ttf');
}