6 Commits

Author SHA1 Message Date
Huffle 0f82a6a915 add distribution bar
Backend Build and Test / build (push) Has been cancelled
2025-05-25 17:03:57 +02:00
Huffle 766b44f353 Merge branch 'main' into AP-63
Backend Build and Test / build (push) Successful in 2m5s
2025-05-25 14:44:01 +02:00
Huffle 92d03d342b add partial css for goals 2025-05-25 14:43:23 +02:00
Huffle cca1f10925 Merge branch 'main' into AP-63 2025-05-25 14:11:41 +02:00
Huffle caf0d6b3fc add css for main goal 2025-05-25 14:11:11 +02:00
Huffle d81ec1f126 Add allowance service and create get endpoint for allowance 2025-05-24 14:11:44 +02:00
52 changed files with 108 additions and 4665 deletions
Binary file not shown.
+7 -199
View File
@@ -437,9 +437,9 @@ func TestPutTaskInvalidTaskId(t *testing.T) {
func TestPostHistory(t *testing.T) { func TestPostHistory(t *testing.T) {
e := startServer(t) e := startServer(t)
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 100, Description: "Add a 100"}).Expect().Status(200) e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 100}).Expect().Status(200)
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 20, Description: "Lolol"}).Expect().Status(200) e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 20}).Expect().Status(200)
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: -10, Description: "Subtracting"}).Expect().Status(200) e.POST("/user/1/history").WithJSON(PostHistory{Allowance: -10}).Expect().Status(200)
response := e.GET("/user/1").Expect().Status(200).JSON().Object() response := e.GET("/user/1").Expect().Status(200).JSON().Object()
response.Value("allowance").Number().IsEqual(100 + 20 - 10) response.Value("allowance").Number().IsEqual(100 + 20 - 10)
@@ -448,36 +448,23 @@ func TestPostHistory(t *testing.T) {
func TestPostHistoryInvalidUserId(t *testing.T) { func TestPostHistoryInvalidUserId(t *testing.T) {
e := startServer(t) e := startServer(t)
e.POST("/user/999/history").WithJSON(PostHistory{Allowance: 100, Description: "Good"}).Expect(). e.POST("/user/999/history").WithJSON(PostHistory{Allowance: 100}).Expect().
Status(404) Status(404)
} }
func TestPostHistoryInvalidDescription(t *testing.T) {
e := startServer(t)
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 100}).Expect().
Status(400)
}
func TestGetHistory(t *testing.T) { func TestGetHistory(t *testing.T) {
e := startServer(t) e := startServer(t)
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 100, Description: "Add 100"}).Expect().Status(200) e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 100}).Expect().Status(200)
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 20, Description: "Add 20"}).Expect().Status(200) e.POST("/user/1/history").WithJSON(PostHistory{Allowance: 20}).Expect().Status(200)
e.POST("/user/1/history").WithJSON(PostHistory{Allowance: -10, Description: "Subtract 10"}).Expect().Status(200) e.POST("/user/1/history").WithJSON(PostHistory{Allowance: -10}).Expect().Status(200)
response := e.GET("/user/1/history").Expect().Status(200).JSON().Array() response := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
response.Length().IsEqual(3) response.Length().IsEqual(3)
response.Value(0).Object().Length().IsEqual(3)
response.Value(0).Object().Value("allowance").Number().IsEqual(100) response.Value(0).Object().Value("allowance").Number().IsEqual(100)
response.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0)) response.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
response.Value(0).Object().Value("description").String().IsEqual("Add 100")
response.Value(1).Object().Value("allowance").Number().IsEqual(20) response.Value(1).Object().Value("allowance").Number().IsEqual(20)
response.Value(1).Object().Value("description").String().IsEqual("Add 20")
response.Value(2).Object().Value("allowance").Number().IsEqual(-10) response.Value(2).Object().Value("allowance").Number().IsEqual(-10)
response.Value(2).Object().Value("description").String().IsEqual("Subtract 10")
} }
func TestGetUserAllowanceById(t *testing.T) { func TestGetUserAllowanceById(t *testing.T) {
@@ -618,36 +605,6 @@ func TestCompleteTask(t *testing.T) {
} }
} }
func TestCompleteTaskWithNoWeights(t *testing.T) {
e := startServer(t)
taskId := createTestTaskWithAmount(e, 101)
e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)
// Ensure main allowance has no weight
e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{
Weight: 0,
}).Expect().Status(200)
// Complete the task
e.POST("/task/" + strconv.Itoa(taskId) + "/complete").Expect().Status(200)
// Verify the task is marked as completed
e.GET("/task/" + strconv.Itoa(taskId)).Expect().Status(404)
// Verify the allowances are updated for user 1
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Length().IsEqual(1)
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
allowances.Value(0).Object().Value("progress").Number().InDelta(101.00, 0.01)
// And also for user 2
allowances = e.GET("/user/2/allowance").Expect().Status(200).JSON().Array()
allowances.Length().IsEqual(1)
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
allowances.Value(0).Object().Value("progress").Number().InDelta(101.00, 0.01)
}
func TestCompleteTaskAllowanceWeightsSumTo0(t *testing.T) { func TestCompleteTaskAllowanceWeightsSumTo0(t *testing.T) {
e := startServer(t) e := startServer(t)
taskId := createTestTaskWithAmount(e, 101) taskId := createTestTaskWithAmount(e, 101)
@@ -686,11 +643,6 @@ func TestCompleteAllowance(t *testing.T) {
createTestTaskWithAmount(e, 100) createTestTaskWithAmount(e, 100)
createTestAllowance(e, "Test Allowance 1", 100, 50) createTestAllowance(e, "Test Allowance 1", 100, 50)
// Update base allowance
e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{
Weight: 0,
}).Expect().Status(200)
// Complete the task // Complete the task
e.POST("/task/1/complete").Expect().Status(200) e.POST("/task/1/complete").Expect().Status(200)
@@ -703,15 +655,10 @@ func TestCompleteAllowance(t *testing.T) {
// Verify history is updated // Verify history is updated
history := e.GET("/user/1/history").Expect().Status(200).JSON().Array() history := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
history.Length().IsEqual(2) history.Length().IsEqual(2)
history.Value(0).Object().Length().IsEqual(3)
history.Value(0).Object().Value("allowance").Number().IsEqual(100) history.Value(0).Object().Value("allowance").Number().IsEqual(100)
history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0)) history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
history.Value(0).Object().Value("description").String().IsEqual("Task completed: Test Task")
history.Value(1).Object().Length().IsEqual(3)
history.Value(1).Object().Value("allowance").Number().IsEqual(-100) history.Value(1).Object().Value("allowance").Number().IsEqual(-100)
history.Value(1).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0)) history.Value(1).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
history.Value(1).Object().Value("description").String().IsEqual("Allowance completed: Test Allowance 1")
} }
func TestCompleteAllowanceInvalidUserId(t *testing.T) { func TestCompleteAllowanceInvalidUserId(t *testing.T) {
@@ -758,145 +705,6 @@ func TestPutBulkAllowance(t *testing.T) {
allowances.Value(2).Object().Value("weight").Number().IsEqual(10) allowances.Value(2).Object().Value("weight").Number().IsEqual(10)
} }
func TestAddAllowanceSimple(t *testing.T) {
e := startServer(t)
createTestAllowance(e, "Test Allowance 1", 1000, 1)
request := map[string]interface{}{
"amount": 10,
"description": "Added to allowance 1",
}
e.POST("/user/1/allowance/1/add").WithJSON(request).Expect().Status(200)
// Verify the allowance is updated
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Value(1).Object().Value("id").Number().IsEqual(1)
allowances.Value(1).Object().Value("progress").Number().InDelta(10.0, 0.01)
// Verify the history is updated
history := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
history.Length().IsEqual(1)
history.Value(0).Object().Value("allowance").Number().InDelta(10.0, 0.01)
history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
history.Value(0).Object().Value("description").String().IsEqual("Added to allowance 1")
}
func TestAddAllowanceWithSpillage(t *testing.T) {
e := startServer(t)
createTestAllowance(e, "Test Allowance 1", 5, 1)
createTestAllowance(e, "Test Allowance 2", 5, 1)
e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{Weight: 1}).Expect().Status(200)
request := map[string]interface{}{
"amount": 10,
"description": "Added to allowance 1",
}
e.POST("/user/1/allowance/1/add").WithJSON(request).Expect().Status(200)
// Verify the allowance is updated
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Value(1).Object().Value("id").Number().IsEqual(1)
allowances.Value(1).Object().Value("progress").Number().InDelta(5.0, 0.01)
allowances.Value(2).Object().Value("id").Number().IsEqual(2)
allowances.Value(2).Object().Value("progress").Number().InDelta(2.5, 0.01)
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
allowances.Value(0).Object().Value("progress").Number().InDelta(2.5, 0.01)
// Verify the history is updated
history := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
history.Length().IsEqual(1)
history.Value(0).Object().Value("allowance").Number().InDelta(10.0, 0.01)
history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
history.Value(0).Object().Value("description").String().IsEqual("Added to allowance 1")
}
func TestAddAllowanceIdZero(t *testing.T) {
e := startServer(t)
createTestAllowance(e, "Test Allowance 1", 1000, 1)
request := map[string]interface{}{
"amount": 10,
"description": "Added to allowance 1",
}
e.POST("/user/1/allowance/0/add").WithJSON(request).Expect().Status(200)
// Verify the allowance is updated
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
allowances.Value(0).Object().Value("progress").Number().InDelta(10.0, 0.01)
// Verify the history is updated
history := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
history.Length().IsEqual(1)
history.Value(0).Object().Value("allowance").Number().InDelta(10.0, 0.01)
history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
history.Value(0).Object().Value("description").String().IsEqual("Added to allowance 1")
}
func TestSubtractAllowanceSimple(t *testing.T) {
e := startServer(t)
createTestAllowance(e, "Test Allowance 1", 1000, 1)
request := map[string]interface{}{
"amount": 10,
"description": "Added to allowance 1",
}
e.POST("/user/1/allowance/1/add").WithJSON(request).Expect().Status(200)
request["amount"] = -2.5
e.POST("/user/1/allowance/1/add").WithJSON(request).Expect().Status(200)
// Verify the allowance is updated
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Value(1).Object().Value("id").Number().IsEqual(1)
allowances.Value(1).Object().Value("progress").Number().InDelta(7.5, 0.01)
// Verify the history is updated
history := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
history.Length().IsEqual(2)
history.Value(0).Object().Value("allowance").Number().InDelta(10.0, 0.01)
history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
history.Value(0).Object().Value("description").String().IsEqual("Added to allowance 1")
history.Value(1).Object().Value("allowance").Number().InDelta(-2.5, 0.01)
history.Value(1).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
history.Value(1).Object().Value("description").String().IsEqual("Added to allowance 1")
}
func TestSubtractllowanceIdZero(t *testing.T) {
e := startServer(t)
createTestAllowance(e, "Test Allowance 1", 1000, 1)
request := map[string]interface{}{
"amount": 10,
"description": "Added to allowance 1",
}
e.POST("/user/1/allowance/0/add").WithJSON(request).Expect().Status(200)
request["amount"] = -2.5
e.POST("/user/1/allowance/0/add").WithJSON(request).Expect().Status(200)
// Verify the allowance is updated
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
allowances.Value(0).Object().Value("progress").Number().InDelta(7.5, 0.01)
// Verify the history is updated
history := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
history.Length().IsEqual(2)
history.Value(0).Object().Value("allowance").Number().InDelta(10.0, 0.01)
history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
history.Value(0).Object().Value("description").String().IsEqual("Added to allowance 1")
history.Value(1).Object().Value("allowance").Number().InDelta(-2.5, 0.01)
history.Value(1).Object().Value("description").String().IsEqual("Added to allowance 1")
}
func getDelta(base time.Time, delta float64) (time.Time, time.Time) { func getDelta(base time.Time, delta float64) (time.Time, time.Time) {
start := base.Add(-time.Duration(delta) * time.Second) start := base.Add(-time.Duration(delta) * time.Second)
end := base.Add(time.Duration(delta) * time.Second) end := base.Add(time.Duration(delta) * time.Second)
+25 -127
View File
@@ -2,7 +2,6 @@ package main
import ( import (
"errors" "errors"
"fmt"
"log" "log"
"math" "math"
"time" "time"
@@ -207,9 +206,8 @@ func (db *Db) CompleteAllowance(userId int, allowanceId int) error {
// Get the cost of the allowance // Get the cost of the allowance
var cost int var cost int
var allowanceName string err = tx.Query("select balance from allowances where id = ? and user_id = ?").
err = tx.Query("select balance, name from allowances where id = ? and user_id = ?"). Bind(allowanceId, userId).ScanSingle(&cost)
Bind(allowanceId, userId).ScanSingle(&cost, &allowanceName)
if err != nil { if err != nil {
return err return err
} }
@@ -222,8 +220,8 @@ func (db *Db) CompleteAllowance(userId int, allowanceId int) error {
} }
// Add a history entry // Add a history entry
err = tx.Query("insert into history (user_id, timestamp, amount, description) values (?, ?, ?, ?)"). err = tx.Query("insert into history (user_id, timestamp, amount) values (?, ?, ?)").
Bind(userId, time.Now().Unix(), -cost, fmt.Sprintf("Allowance completed: %s", allowanceName)). Bind(userId, time.Now().Unix(), -cost).
Exec() Exec()
if err != nil { if err != nil {
return err return err
@@ -422,49 +420,27 @@ func (db *Db) CompleteTask(taskId int) error {
defer tx.MustRollback() defer tx.MustRollback()
var reward int var reward int
var rewardName string err = tx.Query("select reward from tasks where id = ?").Bind(taskId).ScanSingle(&reward)
err = tx.Query("select reward, name from tasks where id = ?").Bind(taskId).ScanSingle(&reward, &rewardName)
if err != nil { if err != nil {
return err return err
} }
for userRow := range tx.Query("select id from users").Range(&err) { for userRow := range tx.Query("select id, weight from users").Range(&err) {
var userId int var userId int
err = userRow.Scan(&userId) var userWeight float64
err = userRow.Scan(&userId, &userWeight)
if err != nil { if err != nil {
return err return err
} }
// Add the history entry // Add the history entry
err = tx.Query("insert into history (user_id, timestamp, amount, description) values (?, ?, ?, ?)"). err = tx.Query("insert into history (user_id, timestamp, amount) values (?, ?, ?)").
Bind(userId, time.Now().Unix(), reward, fmt.Sprintf("Task completed: %s", rewardName)). Bind(userId, time.Now().Unix(), reward).
Exec() Exec()
if err != nil { if err != nil {
return err return err
} }
err := db.addDistributedReward(tx, userId, reward)
if err != nil {
return err
}
}
if err != nil {
return err
}
// Remove the task
err = tx.Query("delete from tasks where id = ?").Bind(taskId).Exec()
return tx.Commit()
}
func (db *Db) addDistributedReward(tx *mysqlite.Tx, userId int, reward int) error {
var userWeight float64
err := tx.Query("select weight from users where id = ?").Bind(userId).ScanSingle(&userWeight)
if err != nil {
return err
}
// Calculate the sums of all weights // Calculate the sums of all weights
var sumOfWeights float64 var sumOfWeights float64
err = tx.Query("select sum(weight) from allowances where user_id = ? and weight > 0").Bind(userId).ScanSingle(&sumOfWeights) err = tx.Query("select sum(weight) from allowances where user_id = ? and weight > 0").Bind(userId).ScanSingle(&sumOfWeights)
@@ -501,7 +477,18 @@ func (db *Db) addDistributedReward(tx *mysqlite.Tx, userId int, reward int) erro
// Add the remaining reward to the user // Add the remaining reward to the user
err = tx.Query("update users set balance = balance + ? where id = ?"). err = tx.Query("update users set balance = balance + ? where id = ?").
Bind(remainingReward, userId).Exec() Bind(remainingReward, userId).Exec()
if err != nil {
return err return err
}
}
if err != nil {
return err
}
// Remove the task
err = tx.Query("delete from tasks where id = ?").Bind(taskId).Exec()
return tx.Commit()
} }
func (db *Db) AddHistory(userId int, allowance *PostHistory) error { func (db *Db) AddHistory(userId int, allowance *PostHistory) error {
@@ -512,8 +499,8 @@ func (db *Db) AddHistory(userId int, allowance *PostHistory) error {
defer tx.MustRollback() defer tx.MustRollback()
amount := int(math.Round(allowance.Allowance * 100.0)) amount := int(math.Round(allowance.Allowance * 100.0))
err = tx.Query("insert into history (user_id, timestamp, amount, description) values (?, ?, ?, ?)"). err = tx.Query("insert into history (user_id, timestamp, amount) values (?, ?, ?)").
Bind(userId, time.Now().Unix(), amount, allowance.Description). Bind(userId, time.Now().Unix(), amount).
Exec() Exec()
if err != nil { if err != nil {
return err return err
@@ -525,11 +512,11 @@ func (db *Db) GetHistory(userId int) ([]History, error) {
history := make([]History, 0) history := make([]History, 0)
var err error var err error
for row := range db.db.Query("select amount, `timestamp`, description from history where user_id = ? order by `timestamp` desc"). for row := range db.db.Query("select amount, `timestamp` from history where user_id = ? order by `timestamp` desc").
Bind(userId).Range(&err) { Bind(userId).Range(&err) {
allowance := History{} allowance := History{}
var timestamp, amount int64 var timestamp, amount int64
err = row.Scan(&amount, &timestamp, &allowance.Description) err = row.Scan(&amount, &timestamp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -542,92 +529,3 @@ func (db *Db) GetHistory(userId int) ([]History, error) {
} }
return history, nil return history, nil
} }
func (db *Db) AddAllowanceAmount(userId int, allowanceId int, request AddAllowanceAmountRequest) error {
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
// Convert amount to integer (cents)
remainingAmount := int(math.Round(request.Amount * 100))
// Insert history entry
err = tx.Query("insert into history (user_id, timestamp, amount, description) values (?, ?, ?, ?)").
Bind(userId, time.Now().Unix(), remainingAmount, request.Description).
Exec()
if err != nil {
return err
}
if allowanceId == 0 {
if remainingAmount < 0 {
var userBalance int
err = tx.Query("select balance from users where id = ?").
Bind(userId).ScanSingle(&userBalance)
if err != nil {
return err
}
if remainingAmount > userBalance {
return fmt.Errorf("cannot remove more than the current balance: %d", userBalance)
}
}
err = tx.Query("update users set balance = balance + ? where id = ?").
Bind(remainingAmount, userId).Exec()
if err != nil {
return err
}
} else if remainingAmount < 0 {
var progress int
err = tx.Query("select balance from allowances where id = ? and user_id = ?").
Bind(allowanceId, userId).ScanSingle(&progress)
if err != nil {
return err
}
if remainingAmount > progress {
return fmt.Errorf("cannot remove more than the current allowance balance: %d", progress)
}
err = tx.Query("update allowances set balance = balance + ? where id = ? and user_id = ?").
Bind(remainingAmount, allowanceId, userId).Exec()
if err != nil {
return err
}
} else {
// Fetch the target and progress of the specified allowance
var target, progress int
err = tx.Query("select target, balance from allowances where id = ? and user_id = ?").
Bind(allowanceId, userId).ScanSingle(&target, &progress)
if err != nil {
return err
}
// Calculate the amount to add to the current allowance
toAdd := remainingAmount
if progress+toAdd > target {
toAdd = target - progress
}
remainingAmount -= toAdd
// Update the current allowance
if toAdd > 0 {
err = tx.Query("update allowances set balance = balance + ? where id = ? and user_id = ?").
Bind(toAdd, allowanceId, userId).Exec()
if err != nil {
return err
}
}
// If there's remaining amount, distribute it to the user's allowances
if remainingAmount > 0 {
err = db.addDistributedReward(tx, userId, remainingAmount)
if err != nil {
return err
}
}
}
return tx.Commit()
}
-7
View File
@@ -16,12 +16,10 @@ type UserWithAllowance struct {
type History struct { type History struct {
Allowance float64 `json:"allowance"` Allowance float64 `json:"allowance"`
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
Description string `json:"description"`
} }
type PostHistory struct { type PostHistory struct {
Allowance float64 `json:"allowance"` Allowance float64 `json:"allowance"`
Description string `json:"description"`
} }
// Task represents a task in the system. // Task represents a task in the system.
@@ -73,8 +71,3 @@ type CreateTaskRequest struct {
type CreateTaskResponse struct { type CreateTaskResponse struct {
ID int `json:"id"` ID int `json:"id"`
} }
type AddAllowanceAmountRequest struct {
Amount float64 `json:"amount"`
Description string `json:"description"`
}
-56
View File
@@ -368,56 +368,6 @@ func completeAllowance(c *gin.Context) {
c.IndentedJSON(http.StatusOK, gin.H{"message": "Allowance completed successfully"}) c.IndentedJSON(http.StatusOK, gin.H{"message": "Allowance completed successfully"})
} }
func addToAllowance(c *gin.Context) {
userIdStr := c.Param("userId")
allowanceIdStr := c.Param("allowanceId")
userId, err := strconv.Atoi(userIdStr)
if err != nil {
log.Printf(ErrInvalidUserID+": %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID})
return
}
allowanceId, err := strconv.Atoi(allowanceIdStr)
if err != nil {
log.Printf("Invalid allowance ID: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid allowance ID"})
return
}
exists, err := db.UserExists(userId)
if err != nil {
log.Printf(ErrCheckingUserExist, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
if !exists {
c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound})
return
}
var allowanceRequest AddAllowanceAmountRequest
if err := c.ShouldBindJSON(&allowanceRequest); err != nil {
log.Printf("Error parsing request body: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
err = db.AddAllowanceAmount(userId, allowanceId, allowanceRequest)
if errors.Is(err, mysqlite.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "Allowance not found"})
return
}
if err != nil {
log.Printf("Error completing allowance: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
c.IndentedJSON(http.StatusOK, gin.H{"message": "Allowance completed successfully"})
}
func createTask(c *gin.Context) { func createTask(c *gin.Context) {
var taskRequest CreateTaskRequest var taskRequest CreateTaskRequest
if err := c.ShouldBindJSON(&taskRequest); err != nil { if err := c.ShouldBindJSON(&taskRequest); err != nil {
@@ -589,11 +539,6 @@ func postHistory(c *gin.Context) {
return return
} }
if historyRequest.Description == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Description cannot be empty"})
return
}
exists, err := db.UserExists(userId) exists, err := db.UserExists(userId)
if err != nil { if err != nil {
log.Printf(ErrCheckingUserExist, err) log.Printf(ErrCheckingUserExist, err)
@@ -661,7 +606,6 @@ func start(ctx context.Context, config *ServerConfig) {
router.DELETE("/api/user/:userId/allowance/:allowanceId", deleteUserAllowance) router.DELETE("/api/user/:userId/allowance/:allowanceId", deleteUserAllowance)
router.PUT("/api/user/:userId/allowance/:allowanceId", putUserAllowance) router.PUT("/api/user/:userId/allowance/:allowanceId", putUserAllowance)
router.POST("/api/user/:userId/allowance/:allowanceId/complete", completeAllowance) router.POST("/api/user/:userId/allowance/:allowanceId/complete", completeAllowance)
router.POST("/api/user/:userId/allowance/:allowanceId/add", addToAllowance)
router.POST("/api/tasks", createTask) router.POST("/api/tasks", createTask)
router.GET("/api/tasks", getTasks) router.GET("/api/tasks", getTasks)
router.GET("/api/task/:taskId", getTask) router.GET("/api/task/:taskId", getTask)
+1 -1
View File
@@ -2,7 +2,7 @@ create table users
( (
id integer primary key, id integer primary key,
name text not null, name text not null,
weight real not null default 10.0, weight real not null default 0.0,
balance integer not null default 0 balance integer not null default 0
) strict; ) strict;
@@ -1 +0,0 @@
update users set weight = 10.0 where weight = 0.0;
@@ -1,2 +0,0 @@
alter table history
add column description text;
File diff suppressed because it is too large Load Diff
@@ -47,7 +47,6 @@
"@angular/cli": "^19.0.0", "@angular/cli": "^19.0.0",
"@angular/compiler-cli": "^19.0.0", "@angular/compiler-cli": "^19.0.0",
"@angular/language-service": "^19.0.0", "@angular/language-service": "^19.0.0",
"@capacitor/assets": "^3.0.5",
"@capacitor/cli": "7.2.0", "@capacitor/cli": "7.2.0",
"@ionic/angular-toolkit": "^12.0.0", "@ionic/angular-toolkit": "^12.0.0",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
@@ -11,6 +11,7 @@ const routes: Routes = [
path: '', path: '',
loadChildren: () => import('./pages/tabs/tabs.module').then(m => m.TabsPageModule) loadChildren: () => import('./pages/tabs/tabs.module').then(m => m.TabsPageModule)
}, },
]; ];
@NgModule({ @NgModule({
imports: [ imports: [
@@ -1,5 +0,0 @@
export interface History {
timestamp: string;
allowance: number;
description: string;
}
@@ -1,17 +0,0 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AddAllowancePage } from './add-allowance.page';
const routes: Routes = [
{
path: '',
component: AddAllowancePage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class AddAllowancePageRoutingModule {}
@@ -1,23 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { AddAllowancePageRoutingModule } from './add-allowance-routing.module';
import { AddAllowancePage } from './add-allowance.page';
import { MatIconModule } from '@angular/material/icon';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
AddAllowancePageRoutingModule,
ReactiveFormsModule,
MatIconModule
],
declarations: [AddAllowancePage]
})
export class AddAllowancePageModule {}
@@ -1,27 +0,0 @@
<ion-header [translucent]="true">
<ion-toolbar>
<div class="toolbar">
<div class="icon" (click)="navigateBack()">
<mat-icon>arrow_back</mat-icon>
</div>
<ion-title *ngIf="isAddMode && goalId == 0">Add to Allowance</ion-title>
<ion-title *ngIf="isAddMode && goalId != 0">Add to Goal</ion-title>
<ion-title *ngIf="!isAddMode">Spend Allowance</ion-title>
</div>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<form [formGroup]="form">
<label>Amount</label>
<input id="amount" type="number" placeholder="0.00" name="price" min="0" value="0" step="0.01" formControlName="amount"/>
<label>Description</label>
<input id="description" type="text" formControlName="description"/>
<button type="button" [disabled]="!form.valid" (click)="changeAllowance()">
<span *ngIf="isAddMode">Add</span>
<span *ngIf="!isAddMode">Spend</span>
</button>
</form>
</ion-content>
@@ -1,40 +0,0 @@
.toolbar {
display: flex;
align-items: center;
}
.icon {
margin-left: 5px;
}
form {
height: 100%;
}
form,
.item {
display: flex;
flex-direction: column;
align-items: center;
}
label {
color: var(--ion-color-primary);
margin-top: 25px;
margin-bottom: 10px;
}
button {
background-color: var(--ion-color-primary);
border-radius: 5px;
color: white;
padding: 10px;
width: 250px;
margin-top: auto;
margin-bottom: 50px;
}
button:disabled,
button[disabled]{
opacity: 0.5;
}
@@ -1,17 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AddAllowancePage } from './add-allowance.page';
describe('AddAllowancePage', () => {
let component: AddAllowancePage;
let fixture: ComponentFixture<AddAllowancePage>;
beforeEach(() => {
fixture = TestBed.createComponent(AddAllowancePage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,51 +0,0 @@
import { Location } from '@angular/common';
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { AllowanceService } from 'src/app/services/allowance.service';
@Component({
selector: 'app-add-allowance',
templateUrl: './add-allowance.page.html',
styleUrls: ['./add-allowance.page.scss'],
standalone: false,
})
export class AddAllowancePage {
public form: FormGroup;
public goalId: number;
public userId: number;
public isAddMode = true;
// Marcus' first comment
// b ........a`.OK ø¶Ópppppppp--P09OP
constructor(
private allowanceService: AllowanceService,
private route: ActivatedRoute,
private formBuilder: FormBuilder,
private router: Router,
private location: Location
) {
this.userId = this.route.snapshot.params['id'];
this.goalId = this.route.snapshot.params['goalId'];
this.form = this.formBuilder.group({
amount: ['', Validators.required],
description: ['', Validators.required]
});
}
changeAllowance() {
this.allowanceService.addOrSpendAllowance(
this.goalId,
this.userId,
this.form.value.amount,
this.form.value.description
);
this.router.navigate(['/tabs/allowance', this.userId]);
}
navigateBack() {
this.location.back();
}
}
@@ -1,17 +0,0 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { SpendllowancePage } from './spend-allowance.page';
const routes: Routes = [
{
path: '',
component: SpendllowancePage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class SpendAllowancePageRoutingModule {}
@@ -1,22 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { SpendAllowancePageRoutingModule } from './spend-allowance-routing.module';
import { SpendllowancePage } from './spend-allowance.page';
import { MatIconModule } from '@angular/material/icon';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
SpendAllowancePageRoutingModule,
ReactiveFormsModule,
MatIconModule
],
declarations: [SpendllowancePage]
})
export class SpendAllowancePageModule {}
@@ -1,52 +0,0 @@
import { Location } from '@angular/common';
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { AllowanceService } from 'src/app/services/allowance.service';
@Component({
selector: 'app-spend-allowance',
templateUrl: './add-allowance.page.html',
styleUrls: ['./add-allowance.page.scss'],
standalone: false,
})
export class SpendllowancePage {
public form: FormGroup;
public goalId: number;
public userId: number;
public isAddMode = false;
constructor(
private allowanceService: AllowanceService,
private route: ActivatedRoute,
private formBuilder: FormBuilder,
private router: Router,
private location: Location
) {
this.userId = this.route.snapshot.params['id'];
this.goalId = this.route.snapshot.params['goalId'];
this.form = this.formBuilder.group({
amount: ['', Validators.required],
description: ['', Validators.required]
});
this.allowanceService.getAllowanceById(this.goalId, this.userId).subscribe(allowance => {
this.form.controls['amount'].addValidators([Validators.max(allowance.progress)]);
});
}
changeAllowance() {
this.allowanceService.addOrSpendAllowance(
this.goalId,
this.userId,
-this.form.value.amount,
this.form.value.description
);
this.router.navigate(['/tabs/allowance', this.userId]);
}
navigateBack() {
this.location.back();
}
}
@@ -6,22 +6,6 @@ const routes: Routes = [
{ {
path: ':id', path: ':id',
component: AllowancePage, component: AllowancePage,
},
{
path: ':id/add',
loadChildren: () => import('../edit-allowance/edit-allowance.module').then(m => m.EditAllowancePageModule)
},
{
path: ':id/edit/:goalId',
loadChildren: () => import('../edit-allowance/edit-allowance.module').then(m => m.EditAllowancePageModule)
},
{
path: ':id/increase/:goalId',
loadChildren: () => import('../add-allowance/add-allowance.module').then(m => m.AddAllowancePageModule)
},
{
path: ':id/spend/:goalId',
loadChildren: () => import('../add-allowance/spend-allowance.module').then(m => m.SpendAllowancePageModule)
} }
]; ];
@@ -1,11 +1,8 @@
<ion-header [translucent]="true" class="ion-no-border"> <ion-header [translucent]="true" class="ion-no-border">
<ion-toolbar> <ion-toolbar>
<div class="toolbar">
<ion-title> <ion-title>
Allowance Allowance
</ion-title> </ion-title>
<button class="top-add-button" (click)="createAllowance()">Add Goal</button>
</div>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
@@ -37,15 +34,15 @@
<div class="main" *ngIf="goal.id === 0; else other_goal"> <div class="main" *ngIf="goal.id === 0; else other_goal">
<div class="title"> <div class="title">
<div class="name">Main Allowance</div> <div class="name">Main Allowance</div>
<div class="icon" (click)="updateAllowance(goal.id)"> <div class="icon">
<mat-icon>settings</mat-icon> <mat-icon>settings</mat-icon>
</div> </div>
</div> </div>
<div class="progress">{{ goal.progress }} SP</div> <div class="progress">{{ goal.progress }} SP</div>
<div class="buttons"> <div class="buttons">
<button class="add-button" (click)="addAllowance(goal.id)">Add</button> <button class="add-button">Add</button>
<!-- <button class="move-button">Move</button> --> <!-- <button class="move-button">Move</button> -->
<button class="spend-button" (click)="spendAllowance(goal.id)">Spend</button> <button class="spend-button">Spend</button>
</div> </div>
</div> </div>
<ng-template #other_goal> <ng-template #other_goal>
@@ -53,15 +50,15 @@
<div> <div>
<div class="title"> <div class="title">
<div class="name">{{ goal.name }}</div> <div class="name">{{ goal.name }}</div>
<div class="icon" (click)="updateAllowance(goal.id)"> <div class="icon">
<mat-icon>settings</mat-icon> <mat-icon>settings</mat-icon>
</div> </div>
</div> </div>
<div class="progress">{{ goal.progress }} / {{ goal.target }} SP</div> <div class="progress">{{ goal.progress }} / {{ goal.target }} SP</div>
<div class="buttons"> <div class="buttons">
<button class="add-button" (click)="addAllowance(goal.id)">Add</button> <button class="add-button">Add</button>
<!-- <button class="move-button">Move</button> --> <!-- <button class="move-button">Move</button> -->
<button class="spend-button" [disabled]="!canFinishGoal(goal)" (click)="completeGoal(goal.id)">Finish goal</button> <button class="spend-button" [disabled]="!canFinishGoal(goal)">Finish goal</button>
</div> </div>
</div> </div>
<div class="color" [style.--background]="hexToRgb(goal.colour)" [style.width.%]="getPercentage(goal)"></div> <div class="color" [style.--background]="hexToRgb(goal.colour)" [style.width.%]="getPercentage(goal)"></div>
@@ -58,10 +58,11 @@ button {
padding-inline: 30px; padding-inline: 30px;
border-radius: 10px; border-radius: 10px;
color: white; color: white;
font-size: 16px;
} }
button:disabled, button:disabled,
button[disabled] { button[disabled]{
opacity: 0.5; opacity: 0.5;
} }
@@ -127,13 +128,3 @@ button[disabled] {
border-radius: 20px; border-radius: 20px;
margin-right: 2px; margin-right: 2px;
} }
.toolbar {
display: flex;
}
.top-add-button {
background-color: var(--ion-color-primary);
margin-right: 15px;
padding-inline: 15px;
}
@@ -1,5 +1,5 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { Allowance } from 'src/app/models/allowance'; import { Allowance } from 'src/app/models/allowance';
import { AllowanceService } from 'src/app/services/allowance.service'; import { AllowanceService } from 'src/app/services/allowance.service';
@@ -14,11 +14,23 @@ import { ViewWillEnter } from '@ionic/angular';
}) })
export class AllowancePage implements ViewWillEnter { export class AllowancePage implements ViewWillEnter {
private id: number; private id: number;
// Move to add/edit page later
private possibleColors: Array<string> = [
'#6199D9',
'#D98B61',
'#DBC307',
'#13DEB5',
'#7DCB7D',
'#CF1DBD',
'#F53311',
'#2F00FF',
'#098B0D',
'#1BC2E8'
];
public allowance$: BehaviorSubject<Array<Allowance>> = new BehaviorSubject<Array<Allowance>>([]); public allowance$: BehaviorSubject<Array<Allowance>> = new BehaviorSubject<Array<Allowance>>([]);
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router,
private allowanceService: AllowanceService private allowanceService: AllowanceService
) { ) {
this.id = this.route.snapshot.params['id']; this.id = this.route.snapshot.params['id'];
@@ -34,9 +46,10 @@ export class AllowancePage implements ViewWillEnter {
this.allowanceService.getAllowanceList(this.id).subscribe(allowance => { this.allowanceService.getAllowanceList(this.id).subscribe(allowance => {
allowance[0].colour = '#9C4BE4'; allowance[0].colour = '#9C4BE4';
allowance[0].name = 'Main Allowance'; allowance[0].name = 'Main Allowance';
console.log('Allowance list: ', allowance);
this.allowance$.next(allowance); this.allowance$.next(allowance);
}) })
}, 50); }, 10);
} }
canFinishGoal(allowance: Allowance): boolean { canFinishGoal(allowance: Allowance): boolean {
@@ -57,30 +70,6 @@ export class AllowancePage implements ViewWillEnter {
for (let allowance of allowanceList) { for (let allowance of allowanceList) {
allowanceTotal += allowance.progress; allowanceTotal += allowance.progress;
} }
if (allowanceTotal === 0) {
return 0;
}
return goal.progress / allowanceTotal * 100; return goal.progress / allowanceTotal * 100;
} }
createAllowance() {
this.router.navigate(['add'], { relativeTo: this.route });
}
updateAllowance(id: number) {
this.router.navigate(['edit', id], { relativeTo: this.route });
}
completeGoal(goalId: number) {
this.allowanceService.completeGoal(goalId, this.id);
this.getAllowance();
}
addAllowance(id: number) {
this.router.navigate(['increase', id], { relativeTo: this.route });
}
spendAllowance(id: number) {
this.router.navigate(['spend', id], { relativeTo: this.route });
}
} }
@@ -1,17 +0,0 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { EditAllowancePage } from './edit-allowance.page';
const routes: Routes = [
{
path: '',
component: EditAllowancePage
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class EditAllowancePageRoutingModule {}
@@ -1,23 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { EditAllowancePageRoutingModule } from './edit-allowance-routing.module';
import { EditAllowancePage } from './edit-allowance.page';
import { MatIconModule } from '@angular/material/icon';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
EditAllowancePageRoutingModule,
ReactiveFormsModule,
MatIconModule
],
declarations: [EditAllowancePage]
})
export class EditAllowancePageModule {}
@@ -1,47 +0,0 @@
<ion-header [translucent]="true">
<ion-toolbar>
<div class="toolbar">
<div class="icon" (click)="navigateBack()">
<mat-icon>arrow_back</mat-icon>
</div>
<ion-title *ngIf="isAddMode">Create Goal</ion-title>
<ion-title *ngIf="!isAddMode && goalId != 0">Edit Goal</ion-title>
<ion-title *ngIf="!isAddMode && goalId == 0">Edit Allowance</ion-title>
<button
*ngIf="!isAddMode && goalId !=0"
class="remove-button"
(click)="deleteAllowance()"
>Delete Goal</button>
</div>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<form [formGroup]="form">
<div class="item" *ngIf="isAddMode || goalId != 0">
<label>Goal Name</label>
<input id="name" type="text" formControlName="name"/>
</div>
<div class="item" *ngIf="isAddMode || goalId != 0">
<label>Target</label>
<input id="target" type="number" placeholder="0.00" name="price" min="0" value="0" step="0.01" formControlName="target"/>
</div>
<label>Weight</label>
<input id="weight" type="number" placeholder="0.00" name="price" min="0" value="0" step="0.01" formControlName="weight"/>
<div class="item" *ngIf="isAddMode || goalId != 0">
<label>Colour</label>
<select formControlName="color">
<option *ngFor="let color of possibleColors" [value]="color" [style.--background]="color">{{color}}</option>
</select>
</div>
<button type="button" [disabled]="!form.valid" (click)="submit()">
<span *ngIf="isAddMode">Add Goal</span>
<span *ngIf="!isAddMode && goalId != 0">Update Goal</span>
<span *ngIf="!isAddMode && goalId == 0">Update Allowance</span>
</button>
</form>
</ion-content>
@@ -1,61 +0,0 @@
.toolbar {
display: flex;
align-items: center;
}
.remove-button {
background-color: var(--ion-color-primary);
margin-right: 15px;
width: 100px;
margin-bottom: 0;
}
form {
height: 100%;
}
form,
.item {
display: flex;
flex-direction: column;
align-items: center;
}
label {
color: var(--ion-color-primary);
margin-top: 25px;
margin-bottom: 10px;
}
input,
select {
border: 1px solid var(--ion-color-primary);
border-radius: 5px;
width: 250px;
}
option {
--background: white;
background-color: var(--background);
color: var(--background);
font-family: var(--ion-font-family);
}
button {
background-color: var(--ion-color-primary);
border-radius: 5px;
color: white;
padding: 10px;
width: 250px;
margin-top: auto;
margin-bottom: 50px;
}
button:disabled,
button[disabled]{
opacity: 0.5;
}
.icon {
margin-left: 5px;
}
@@ -1,17 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EditAllowancePage } from './edit-allowance.page';
describe('EditAllowancePage', () => {
let component: EditAllowancePage;
let fixture: ComponentFixture<EditAllowancePage>;
beforeEach(() => {
fixture = TestBed.createComponent(EditAllowancePage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,107 +0,0 @@
import { Location } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { AllowanceService } from 'src/app/services/allowance.service';
@Component({
selector: 'app-edit-allowance',
templateUrl: './edit-allowance.page.html',
styleUrls: ['./edit-allowance.page.scss'],
standalone: false
})
export class EditAllowancePage implements OnInit {
public form: FormGroup;
public goalId: number;
public userId: number;
public isAddMode: boolean;
public possibleColors: Array<string> = [
'#6199D9',
'#D98B61',
'#DBC307',
'#13DEB5',
'#7DCB7D',
'#CF1DBD',
'#F53311',
'#2F00FF',
'#098B0D',
'#1BC2E8'
];
constructor(
private route: ActivatedRoute,
private formBuilder: FormBuilder,
private allowanceService: AllowanceService,
private router: Router,
private location: Location
) {
this.userId = this.route.snapshot.params['id'];
this.goalId = this.route.snapshot.params['goalId'];
this.isAddMode = !this.goalId;
this.allowanceService.getAllowanceList(this.userId).subscribe((list) => {
for (let allowance of list) {
this.possibleColors = this.possibleColors.filter(color => color !== allowance.colour);
if (!this.isAddMode && +this.goalId === allowance.id) {
this.possibleColors.unshift(allowance.colour);
}
}
});
this.form = this.formBuilder.group({
name: ['', Validators.required],
target: ['', Validators.required],
weight: ['', Validators.required],
color: ['', Validators.required]
});
}
ngOnInit() {
if (!this.isAddMode) {
this.allowanceService.getAllowanceById(this.goalId, this.userId).subscribe((allowance) => {
if (+this.goalId === 0) {
this.form.setValue({
name: 'Main Allowance',
target: 0,
weight: allowance.weight,
color: '#9C4BE4'
});
} else {
this.form.setValue({
name: allowance.name,
target: allowance.target,
weight: allowance.weight,
color: allowance.colour
});
}
});
}
}
submit() {
const formValue = this.form.value;
const allowance = {
name: formValue.name,
target: formValue.target,
weight: formValue.weight,
colour: formValue.color,
};
if (this.isAddMode) {
this.allowanceService.createAllowance(allowance, this.userId);
} else {
this.allowanceService.updateAllowance(allowance, this.goalId, this.userId);
}
this.router.navigate(['/tabs/allowance', this.userId]);
}
deleteAllowance() {
this.allowanceService.deleteAllowance(this.goalId, this.userId);
this.router.navigate(['/tabs/allowance', this.userId]);
}
navigateBack() {
this.location.back();
}
}
@@ -7,7 +7,6 @@ import { IonicModule } from '@ionic/angular';
import { EditTaskPageRoutingModule } from './edit-task-routing.module'; import { EditTaskPageRoutingModule } from './edit-task-routing.module';
import { EditTaskPage } from './edit-task.page'; import { EditTaskPage } from './edit-task.page';
import { MatIconModule } from '@angular/material/icon';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -15,8 +14,7 @@ import { MatIconModule } from '@angular/material/icon';
FormsModule, FormsModule,
IonicModule, IonicModule,
EditTaskPageRoutingModule, EditTaskPageRoutingModule,
ReactiveFormsModule, ReactiveFormsModule
MatIconModule
], ],
declarations: [EditTaskPage] declarations: [EditTaskPage]
}) })
@@ -1,9 +1,6 @@
<ion-header [translucent]="true"> <ion-header [translucent]="true">
<ion-toolbar> <ion-toolbar>
<div class="toolbar"> <div class="toolbar">
<div class="icon" (click)="navigateBack()">
<mat-icon>arrow_back</mat-icon>
</div>
<ion-title *ngIf="isAddMode">Create Task</ion-title> <ion-title *ngIf="isAddMode">Create Task</ion-title>
<ion-title *ngIf="!isAddMode">Edit Task</ion-title> <ion-title *ngIf="!isAddMode">Edit Task</ion-title>
<button <button
@@ -21,7 +18,7 @@
<input id="name" type="text" formControlName="name"/> <input id="name" type="text" formControlName="name"/>
<label>Reward</label> <label>Reward</label>
<input id="reward" type="number" placeholder="0.00" name="price" min="0" value="0" step="0.01" formControlName="reward"/> <input id="name" type="number" placeholder="0.00" name="price" min="0" value="0" step="0.01" formControlName="reward"/>
<label>Assigned</label> <label>Assigned</label>
<select formControlName="assigned"> <select formControlName="assigned">
@@ -1,12 +1,11 @@
.toolbar { .toolbar {
display: flex; display: flex;
align-items: center;
} }
.remove-button { .remove-button {
background-color: var(--ion-color-primary); background-color: var(--ion-color-primary);
margin-right: 15px; margin-right: 15px;
width: 95px; width: 85px;
margin-bottom: 0; margin-bottom: 0;
} }
@@ -44,7 +43,3 @@ button:disabled,
button[disabled]{ button[disabled]{
opacity: 0.5; opacity: 0.5;
} }
.icon {
margin-left: 5px;
}
@@ -23,8 +23,7 @@ export class EditTaskPage implements OnInit {
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private taskService: TaskService, private taskService: TaskService,
private userService: UserService, private userService: UserService,
private router: Router, private router: Router
private location: Location
) { ) {
this.id = this.route.snapshot.params['id']; this.id = this.route.snapshot.params['id'];
this.isAddMode = !this.id; this.isAddMode = !this.id;
@@ -78,8 +77,4 @@ export class EditTaskPage implements OnInit {
this.taskService.deleteTask(this.id); this.taskService.deleteTask(this.id);
this.router.navigate(['/tabs/tasks']); this.router.navigate(['/tabs/tasks']);
} }
navigateBack() {
this.location.back();
}
} }
@@ -5,8 +5,6 @@ import { FormsModule } from '@angular/forms';
import { HistoryPage } from './history.page'; import { HistoryPage } from './history.page';
import { HistoryPageRoutingModule } from './history-routing.module'; import { HistoryPageRoutingModule } from './history-routing.module';
import { provideHttpClient } from '@angular/common/http';
import { HistoryService } from 'src/app/services/history.service';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -15,10 +13,6 @@ import { HistoryService } from 'src/app/services/history.service';
FormsModule, FormsModule,
HistoryPageRoutingModule HistoryPageRoutingModule
], ],
declarations: [HistoryPage], declarations: [HistoryPage]
providers: [
provideHttpClient(),
HistoryService
]
}) })
export class HistoryPageModule {} export class HistoryPageModule {}
@@ -7,14 +7,5 @@
</ion-header> </ion-header>
<ion-content> <ion-content>
<div class="item" *ngFor="let history of history$ | async">
<div class="left">
<div class="date">{{ history.timestamp | date: 'yyyy-MM-dd' }}</div>
<div class="description">{{ history.description }}</div>
</div>
<div
class="amount"
[ngClass]="{ 'negative': history.allowance < 0 }"
>{{ history.allowance }} SP</div>
</div>
</ion-content> </ion-content>
@@ -1,29 +0,0 @@
.item {
display: flex;
flex-direction: row;
align-items: center;
border-bottom: 1px solid var(--line-color);
padding: 8px;
}
.left {
width: 70%;
}
.date {
color: var(--line-color);
}
.description {
color: var(--font-color);
}
.amount {
margin-left: auto;
font-size: 22px;
color: var(--positive-amount-color);
}
.negative {
color: var(--negative-amount-color);
}
@@ -1,9 +1,4 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ViewWillEnter } from '@ionic/angular';
import { BehaviorSubject } from 'rxjs';
import { History } from 'src/app/models/history';
import { HistoryService } from 'src/app/services/history.service';
@Component({ @Component({
selector: 'app-history', selector: 'app-history',
@@ -11,28 +6,8 @@ import { HistoryService } from 'src/app/services/history.service';
styleUrls: ['history.page.scss'], styleUrls: ['history.page.scss'],
standalone: false, standalone: false,
}) })
export class HistoryPage implements ViewWillEnter { export class HistoryPage {
userId: number;
public history$: BehaviorSubject<Array<History>> = new BehaviorSubject<Array<History>>([]);
constructor() {}
constructor(
private route: ActivatedRoute,
private historyService: HistoryService
) {
this.userId = this.route.snapshot.params['id'];
this.getHistory();
}
ionViewWillEnter(): void {
this.getHistory();
}
getHistory() {
setTimeout(() => {
this.historyService.getHistoryList(this.userId).subscribe(history => {
this.history$.next(history);
})
}, 20);
}
} }
@@ -8,7 +8,7 @@ const routes: Routes = [
component: TabsPage, component: TabsPage,
children: [ children: [
{ {
path: 'history/:id', path: 'history',
loadChildren: () => import('../history/history.module').then(m => m.HistoryPageModule) loadChildren: () => import('../history/history.module').then(m => m.HistoryPageModule)
}, },
{ {
@@ -1,6 +1,6 @@
<ion-tabs> <ion-tabs>
<ion-tab-bar slot="bottom"> <ion-tab-bar slot="bottom">
<ion-tab-button [tab]="historyTab" [href]="historyNav"> <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">
@@ -1,5 +1,4 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { StorageService } from 'src/app/services/storage.service';
@Component({ @Component({
selector: 'app-tabs', selector: 'app-tabs',
@@ -8,16 +7,6 @@ import { StorageService } from 'src/app/services/storage.service';
standalone: false, standalone: false,
}) })
export class TabsPage { export class TabsPage {
historyNav = ''; constructor() {}
historyTab = '';
constructor(private storageService: StorageService) {
this.storageService.getCurrentUserId().then((userId) => {
if (userId !== undefined && userId !== null) {
this.historyNav = `/tabs/history/${userId}`;
this.historyTab = `history/${userId}`;
}
});
}
} }
@@ -11,15 +11,14 @@
<ion-content> <ion-content>
<div class="content"> <div class="content">
<!-- <div class="icon"> <div class="icon">
<mat-icon>filter_alt</mat-icon> <mat-icon>filter_alt</mat-icon>
</div> --> </div>
<div class="list"> <div class="list">
<div class="task" *ngFor="let task of tasks$ | async"> <div class="task" *ngFor="let task of tasks$ | async">
<button (click)="completeTask(task.id)">Done</button> <button (click)="completeTask(task.id)">Done</button>
<div (click)="updateTask(task.id)" class="item"> <div (click)="updateTask(task.id)" class="item">
<div class="name">{{ task.name }}</div> <div class="name">{{ task.name }}</div>
<div class="assigned">{{ usernames[task.assigned ? task.assigned : 0] }}</div>
<div <div
class="reward" class="reward"
[ngClass]="{ 'negative': task.reward < 0 }" [ngClass]="{ 'negative': task.reward < 0 }"
@@ -68,9 +68,3 @@ button {
margin-right: 15px; margin-right: 15px;
width: 75px; width: 75px;
} }
.assigned {
color: var(--line-color);
margin-left: 3px;
font-size: 12px;
}
@@ -14,7 +14,6 @@ import { ViewWillEnter } from '@ionic/angular';
}) })
export class TasksPage implements ViewWillEnter { export class TasksPage implements ViewWillEnter {
public tasks$: BehaviorSubject<Array<Task>> = new BehaviorSubject<Array<Task>>([]); public tasks$: BehaviorSubject<Array<Task>> = new BehaviorSubject<Array<Task>>([]);
public usernames = ['', 'See', 'Huffle']
constructor( constructor(
private taskService: TaskService, private taskService: TaskService,
@@ -33,7 +32,7 @@ export class TasksPage implements ViewWillEnter {
this.taskService.getTaskList().subscribe(tasks => { this.taskService.getTaskList().subscribe(tasks => {
this.tasks$.next(tasks); this.tasks$.next(tasks);
}); });
}, 50); }, 10);
} }
createTask() { createTask() {
@@ -7,35 +7,11 @@ import { Allowance } from '../models/allowance';
providedIn: 'root' providedIn: 'root'
}) })
export class AllowanceService { export class AllowanceService {
private url = 'https://allowanceplanner.seeseepuff.be/api'; private url = 'http://localhost:8080/api';
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
getAllowanceList(userId: number): Observable<Array<Allowance>> { getAllowanceList(userId: number): Observable<Array<Allowance>> {
return this.http.get<Allowance[]>(`${this.url}/user/${userId}/allowance`); return this.http.get<Allowance[]>(`${this.url}/user/${userId}/allowance`);
} }
getAllowanceById(allowanceId: number, userId: number): Observable<Allowance> {
return this.http.get<Allowance>(`${this.url}/user/${userId}/allowance/${allowanceId}`);
}
createAllowance(allowance: Partial<Allowance>, userId: number) {
this.http.post(`${this.url}/user/${userId}/allowance`, allowance).subscribe();
}
updateAllowance(allowance: Partial<Allowance>, allowanceId: number, userId: number) {
this.http.put(`${this.url}/user/${userId}/allowance/${allowanceId}`, allowance).subscribe();
}
deleteAllowance(allowanceId: number, userId: number) {
this.http.delete(`${this.url}/user/${userId}/allowance/${allowanceId}`).subscribe();
}
completeGoal(goalId: number, userId: number) {
this.http.post(`${this.url}/user/${userId}/allowance/${goalId}/complete`, {}).subscribe();
}
addOrSpendAllowance(goalId: number, userId: number, amount: number, description: string) {
this.http.post(`${this.url}/user/${userId}/allowance/${goalId}/add`, { amount, description }).subscribe();
}
} }
@@ -1,17 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { History } from '../models/history';
@Injectable({
providedIn: 'root'
})
export class HistoryService {
private url = 'https://allowanceplanner.seeseepuff.be/api';
constructor(private http: HttpClient) {}
getHistoryList(userId: number): Observable<Array<History>> {
return this.http.get<History[]>(`${this.url}/user/${userId}/history`);
}
}
@@ -7,7 +7,7 @@ import { Task } from '../models/task';
providedIn: 'root' providedIn: 'root'
}) })
export class TaskService { export class TaskService {
private url = 'https://allowanceplanner.seeseepuff.be/api'; private url = 'http://localhost:8080/api';
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
@@ -7,7 +7,7 @@ import { User } from '../models/user';
providedIn: 'root', providedIn: 'root',
}) })
export class UserService { export class UserService {
private url = 'https://allowanceplanner.seeseepuff.be/api'; private url = 'http://localhost:8080/api';
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
getUserList(): Observable<Array<User>> { getUserList(): Observable<Array<User>> {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

@@ -43,7 +43,3 @@ ion-title {
ion-header { ion-header {
border-bottom: 1px solid var(--line-color); border-bottom: 1px solid var(--line-color);
} }
button {
font-size: 16px;
}