2 Commits

Author SHA1 Message Date
49d9ff467a Improve compatibility with ancient browsers
All checks were successful
Backend Build and Test / build (push) Successful in 2m9s
2025-05-25 19:25:39 +02:00
56ea895cf8 Ignore more sqlite files 2025-05-25 19:25:39 +02:00
87 changed files with 258 additions and 5311 deletions

View File

@@ -14,19 +14,3 @@ In order to run the frontend, go to the `allowance-planner-v2` directory in the
```bash ```bash
$ ionic serve $ ionic serve
``` ```
## Running frontend
In order to build the frontend for android, go to the `allowance-planner-v2` directory in the `frontend` directory and run:
```bash
$ ionic capacitor build android
```
## Backend links
```bash
Main: https://allowanceplanner.seeseepuff.be/api
```
```bash
Test: http://localhost:8080/api
```

Binary file not shown.

View File

@@ -2,11 +2,10 @@ package main
import ( import (
"fmt" "fmt"
"github.com/gavv/httpexpect/v2"
"strconv" "strconv"
"testing" "testing"
"time" "time"
"github.com/gavv/httpexpect/v2"
) )
const ( const (
@@ -16,9 +15,8 @@ const (
func startServer(t *testing.T) *httpexpect.Expect { func startServer(t *testing.T) *httpexpect.Expect {
config := ServerConfig{ config := ServerConfig{
Datasource: ":memory:", Datasource: ":memory:",
//Datasource: "test.db", Addr: ":0",
Addr: ":0", Started: make(chan bool),
Started: make(chan bool),
} }
go start(t.Context(), &config) go start(t.Context(), &config)
<-config.Started <-config.Started
@@ -286,54 +284,6 @@ func TestCreateTask(t *testing.T) {
responseWithUser.Value("id").Number().IsEqual(2) responseWithUser.Value("id").Number().IsEqual(2)
} }
//func TestCreateScheduleTask(t *testing.T) {
// e := startServer(t)
//
// // Create a new task without assigned user
// requestBody := map[string]interface{}{
// "name": "Test Task",
// "reward": 100,
// "schedule": "0 */5 * * * *",
// }
//
// response := e.POST("/tasks").
// WithJSON(requestBody).
// Expect().
// Status(201). // Expect Created status
// JSON().Object()
//
// requestBody["schedule"] = "every 5 seconds"
// e.POST("/tasks").WithJSON(requestBody).Expect().Status(400)
//
// // Verify the response has an ID
// response.ContainsKey("id")
// response.Value("id").Number().IsEqual(1)
//
// e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)
//
// // Get task
// result := e.GET("/task/1").Expect().Status(200).JSON().Object()
// result.Value("id").IsEqual(1)
// result.Value("name").IsEqual("Test Task")
// result.Value("schedule").IsEqual("0 */5 * * * *")
// result.Value("reward").IsEqual(100)
// result.Value("assigned").IsNull()
//
// // Complete the task
// e.POST("/task/1/complete").Expect().Status(200)
//
// // Set expires date to 1 second in the past
// db.db.Query("update tasks set next_run = ? where id = 1").Bind(time.Now().Add(10 * -time.Minute).Unix()).MustExec()
//
// // Verify a new task is created
// newTask := e.GET("/task/2").Expect().Status(200).JSON().Object()
// newTask.Value("id").IsEqual(2)
// newTask.Value("name").IsEqual("Test Task")
// newTask.Value("schedule").IsEqual("0 */5 * * * *")
// newTask.Value("reward").IsEqual(100)
// newTask.Value("assigned").IsNull()
//}
func TestDeleteTask(t *testing.T) { func TestDeleteTask(t *testing.T) {
e := startServer(t) e := startServer(t)
@@ -487,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)
@@ -498,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) {
@@ -753,15 +690,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) {
@@ -808,145 +740,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)
@@ -964,88 +757,3 @@ func createTestAllowance(e *httpexpect.Expect, name string, target float64, weig
func createTestTask(e *httpexpect.Expect) int { func createTestTask(e *httpexpect.Expect) int {
return createTestTaskWithAmount(e, 100) return createTestTaskWithAmount(e, 100)
} }
// Transfer tests
func TestTransferSuccessful(t *testing.T) {
e := startServer(t)
// Create two allowances for user 1
createTestAllowance(e, "From Allowance", 100, 1)
createTestAllowance(e, "To Allowance", 100, 1)
// Add 30 to allowance 1
req := map[string]interface{}{"amount": 30, "description": "funds"}
e.POST("/user/1/allowance/1/add").WithJSON(req).Expect().Status(200)
// Transfer 10 from 1 to 2
transfer := map[string]interface{}{"from": 1, "to": 2, "amount": 10}
e.POST("/transfer").WithJSON(transfer).Expect().Status(200).JSON().Object().Value("message").IsEqual("Transfer successful")
// Verify balances
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Value(1).Object().Value("progress").Number().InDelta(20.0, 0.01)
allowances.Value(2).Object().Value("progress").Number().InDelta(10.0, 0.01)
}
func TestTransferCapsAtTarget(t *testing.T) {
e := startServer(t)
// Create two allowances
createTestAllowance(e, "From Allowance", 100, 1)
createTestAllowance(e, "To Allowance", 5, 1)
// Add 10 to allowance 1
req := map[string]interface{}{"amount": 10, "description": "funds"}
e.POST("/user/1/allowance/1/add").WithJSON(req).Expect().Status(200)
// Transfer 10 from 1 to 2, but to only needs 5
transfer := map[string]interface{}{"from": 1, "to": 2, "amount": 10}
e.POST("/transfer").WithJSON(transfer).Expect().Status(200)
// Verify capped transfer
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Value(1).Object().Value("progress").Number().InDelta(5.0, 0.01) // from had 10, transferred 5 -> left 5
allowances.Value(2).Object().Value("progress").Number().InDelta(5.0, 0.01) // to reached target
}
func TestTransferDifferentUsersFails(t *testing.T) {
e := startServer(t)
// Create allowance for user 1 and user 2
createTestAllowance(e, "User1 Allowance", 100, 1)
// create for user 2
e.POST("/user/2/allowance").WithJSON(CreateAllowanceRequest{Name: "User2 Allowance", Target: 100, Weight: 1}).Expect().Status(201)
// Add to user1 allowance
req := map[string]interface{}{"amount": 10, "description": "funds"}
e.POST("/user/1/allowance/1/add").WithJSON(req).Expect().Status(200)
// Attempt transfer between different users
transfer := map[string]interface{}{"from": 1, "to": 1 /* wrong id to simulate different user's id? */}
// To ensure different user, fetch the allowance id for user2 (it's 1 for user2 in its own context but global id will be 2)
// Create above for user2 produced global id 2, so use that
transfer = map[string]interface{}{"from": 1, "to": 2, "amount": 5}
e.POST("/transfer").WithJSON(transfer).Expect().Status(400)
}
func TestTransferInsufficientFunds(t *testing.T) {
e := startServer(t)
// Create two allowances
createTestAllowance(e, "From Allowance", 100, 1)
createTestAllowance(e, "To Allowance", 100, 1)
// Ensure from has 0 balance
transfer := map[string]interface{}{"from": 1, "to": 2, "amount": 10}
resp := e.POST("/transfer").WithJSON(transfer).Expect().Status(400).JSON().Object()
// Error text should mention insufficient funds
resp.Value("error").String().ContainsFold("insufficient")
}
func TestTransferNotFound(t *testing.T) {
e := startServer(t)
// No allowances exist yet (only user rows). Attempt transfer with non-existent IDs
transfer := map[string]interface{}{"from": 999, "to": 1000, "amount": 1}
e.POST("/transfer").WithJSON(transfer).Expect().Status(404)
}

View File

@@ -2,8 +2,6 @@ package main
import ( import (
"errors" "errors"
"fmt"
"github.com/adhocore/gronx"
"log" "log"
"math" "math"
"time" "time"
@@ -208,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
} }
@@ -223,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
@@ -314,20 +311,10 @@ func (db *Db) CreateTask(task *CreateTaskRequest) (int, error) {
} }
defer tx.MustRollback() defer tx.MustRollback()
var nextRun *int64
if task.Schedule != nil {
nextRunTime, err := gronx.NextTick(*task.Schedule, false)
if err != nil {
return 0, fmt.Errorf("failed to calculate next run: %w", err)
}
nextRunTimeAsInt := nextRunTime.Unix()
nextRun = &nextRunTimeAsInt
}
// Insert the new task // Insert the new task
reward := int(math.Round(task.Reward * 100.0)) reward := int(math.Round(task.Reward * 100.0))
err = tx.Query("insert into tasks (name, reward, assigned, schedule, next_run) values (?, ?, ?, ?, ?)"). err = tx.Query("insert into tasks (name, reward, assigned) values (?, ?, ?)").
Bind(task.Name, reward, task.Assigned, task.Schedule, nextRun). Bind(task.Name, reward, task.Assigned).
Exec() Exec()
if err != nil { if err != nil {
@@ -351,17 +338,13 @@ func (db *Db) CreateTask(task *CreateTaskRequest) (int, error) {
} }
func (db *Db) GetTasks() ([]Task, error) { func (db *Db) GetTasks() ([]Task, error) {
err := db.UpdateScheduledTasks()
if err != nil {
return nil, fmt.Errorf("failed to update scheduled tasks: %w", err)
}
tasks := make([]Task, 0) tasks := make([]Task, 0)
var err error
for row := range db.db.Query("select id, name, reward, assigned, schedule from tasks where completed is null").Range(&err) { for row := range db.db.Query("select id, name, reward, assigned from tasks").Range(&err) {
task := Task{} task := Task{}
var reward int64 var reward int64
err = row.Scan(&task.ID, &task.Name, &reward, &task.Assigned, &task.Schedule) err = row.Scan(&task.ID, &task.Name, &reward, &task.Assigned)
task.Reward = float64(reward) / 100.0 task.Reward = float64(reward) / 100.0
if err != nil { if err != nil {
return nil, err return nil, err
@@ -377,78 +360,16 @@ func (db *Db) GetTasks() ([]Task, error) {
func (db *Db) GetTask(id int) (Task, error) { func (db *Db) GetTask(id int) (Task, error) {
task := Task{} task := Task{}
err := db.UpdateScheduledTasks()
if err != nil {
return Task{}, fmt.Errorf("failed to update scheduled tasks: %w", err)
}
var reward int64 var reward int64
err = db.db.Query("select id, name, reward, assigned, schedule from tasks where id = ? and completed is null"). err := db.db.Query("select id, name, reward, assigned from tasks where id = ?").
Bind(id).ScanSingle(&task.ID, &task.Name, &reward, &task.Assigned, &task.Schedule) Bind(id).ScanSingle(&task.ID, &task.Name, &reward, &task.Assigned)
if err != nil {
return task, err
}
task.Reward = float64(reward) / 100.0 task.Reward = float64(reward) / 100.0
if err != nil {
return Task{}, err
}
return task, nil return task, nil
} }
func (db *Db) UpdateScheduledTasks() error {
type ScheduledTask struct {
ID int
Schedule string
Expires int64
}
tasks := make([]ScheduledTask, 0)
var err error
for row := range db.db.Query("select id, schedule, next_run from tasks where schedule is not null").Range(&err) {
task := ScheduledTask{}
err := row.Scan(&task.ID, &task.Schedule, &task.Expires)
if err != nil {
return err
}
if time.Now().Unix() >= task.Expires {
tasks = append(tasks, task)
}
}
if err != nil {
return fmt.Errorf("failed to fetch scheduled tasks: %w", err)
}
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
for _, task := range tasks {
nextRun, err := gronx.NextTickAfter(task.Schedule, time.Now(), false)
if err != nil {
return fmt.Errorf("failed to calculate next run for task %d: %w", task.ID, err)
}
err = tx.Query("insert into tasks (name, reward, assigned, schedule, next_run) select name, reward, assigned, schedule, ? from tasks where id = ?").
Bind(nextRun.Unix(), task.ID).
Exec()
if err != nil {
return err
}
err = tx.Query("update tasks set schedule = null where id = ?").Bind(task.ID).Exec()
if err != nil {
return err
}
tx.Query("select last_insert_rowid()").MustScanSingle(&task.ID)
log.Printf("Task %d scheduled for %s", task.ID, nextRun)
}
if err != nil {
return err
}
return tx.Commit()
}
func (db *Db) DeleteTask(id int) error { func (db *Db) DeleteTask(id int) error {
tx, err := db.db.Begin() tx, err := db.db.Begin()
if err != nil { if err != nil {
@@ -499,28 +420,63 @@ 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) // Calculate the sums of all weights
var sumOfWeights float64
err = tx.Query("select sum(weight) from allowances where user_id = ? and weight > 0").Bind(userId).ScanSingle(&sumOfWeights)
sumOfWeights += userWeight
remainingReward := reward
if sumOfWeights > 0 {
// Distribute the reward to the allowances
for allowanceRow := range tx.Query("select id, weight, target, balance from allowances where user_id = ? and weight > 0 order by (target - balance) asc").Bind(userId).Range(&err) {
var allowanceId, allowanceTarget, allowanceBalance int
var allowanceWeight float64
err = allowanceRow.Scan(&allowanceId, &allowanceWeight, &allowanceTarget, &allowanceBalance)
if err != nil {
return err
}
// Calculate the amount to add to the allowance
amount := int((allowanceWeight / sumOfWeights) * float64(remainingReward))
if allowanceBalance+amount > allowanceTarget {
// If the amount reaches past the target, set it to the target
amount = allowanceTarget - allowanceBalance
}
sumOfWeights -= allowanceWeight
err = tx.Query("update allowances set balance = balance + ? where id = ? and user_id = ?").
Bind(amount, allowanceId, userId).Exec()
if err != nil {
return err
}
remainingReward -= amount
}
}
// Add the remaining reward to the user
err = tx.Query("update users set balance = balance + ? where id = ?").
Bind(remainingReward, userId).Exec()
if err != nil { if err != nil {
return err return err
} }
@@ -530,60 +486,11 @@ func (db *Db) CompleteTask(taskId int) error {
} }
// Remove the task // Remove the task
err = tx.Query("update tasks set completed=? where id = ?").Bind(time.Now().Unix(), taskId).Exec() err = tx.Query("delete from tasks where id = ?").Bind(taskId).Exec()
if err != nil {
return err
}
return tx.Commit() return tx.Commit()
} }
func (db *Db) addDistributedReward(tx *mysqlite.Tx, userId int, reward int) error {
var userWeight float64
err := tx.Query("select weight from users where id = ?").Bind(userId).ScanSingle(&userWeight)
if err != nil {
return err
}
// Calculate the sums of all weights
var sumOfWeights float64
err = tx.Query("select sum(weight) from allowances where user_id = ? and weight > 0").Bind(userId).ScanSingle(&sumOfWeights)
sumOfWeights += userWeight
remainingReward := reward
if sumOfWeights > 0 {
// Distribute the reward to the allowances
for allowanceRow := range tx.Query("select id, weight, target, balance from allowances where user_id = ? and weight > 0 order by (target - balance) asc").Bind(userId).Range(&err) {
var allowanceId, allowanceTarget, allowanceBalance int
var allowanceWeight float64
err = allowanceRow.Scan(&allowanceId, &allowanceWeight, &allowanceTarget, &allowanceBalance)
if err != nil {
return err
}
// Calculate the amount to add to the allowance
amount := int((allowanceWeight / sumOfWeights) * float64(remainingReward))
if allowanceBalance+amount > allowanceTarget {
// If the amount reaches past the target, set it to the target
amount = allowanceTarget - allowanceBalance
}
sumOfWeights -= allowanceWeight
err = tx.Query("update allowances set balance = balance + ? where id = ? and user_id = ?").
Bind(amount, allowanceId, userId).Exec()
if err != nil {
return err
}
remainingReward -= amount
}
}
// Add the remaining reward to the user
err = tx.Query("update users set balance = balance + ? where id = ?").
Bind(remainingReward, userId).Exec()
return err
}
func (db *Db) AddHistory(userId int, allowance *PostHistory) error { func (db *Db) AddHistory(userId int, allowance *PostHistory) error {
tx, err := db.db.Begin() tx, err := db.db.Begin()
if err != nil { if err != nil {
@@ -592,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
@@ -605,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
} }
@@ -622,160 +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()
}
func (db *Db) TransferAllowance(fromId int, toId int, amount float64) error {
if fromId == toId {
return nil
}
amountCents := int(math.Round(amount * 100.0))
if amountCents <= 0 {
return fmt.Errorf("amount must be positive")
}
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
// Fetch from allowance (user_id, balance)
var fromUserId int
var fromBalance int
err = tx.Query("select user_id, balance from allowances where id = ?").Bind(fromId).ScanSingle(&fromUserId, &fromBalance)
if err != nil {
return err
}
// Fetch to allowance (user_id, target, balance)
var toUserId int
var toTarget int
var toBalance int
err = tx.Query("select user_id, target, balance from allowances where id = ?").Bind(toId).ScanSingle(&toUserId, &toTarget, &toBalance)
if err != nil {
return err
}
// Ensure same owner
if fromUserId != toUserId {
return fmt.Errorf(ErrDifferentUsers)
}
// Calculate how much the 'to' goal still needs
remainingTo := toTarget - toBalance
if remainingTo <= 0 {
// Nothing to transfer
return fmt.Errorf("target already reached")
}
// Limit transfer to what 'to' still needs
transfer := amountCents
if transfer > remainingTo {
transfer = remainingTo
}
// Ensure 'from' has enough balance
if fromBalance < transfer {
return fmt.Errorf(ErrInsufficientFunds)
}
// Perform updates
err = tx.Query("update allowances set balance = balance - ? where id = ? and user_id = ?").Bind(transfer, fromId, fromUserId).Exec()
if err != nil {
return err
}
err = tx.Query("update allowances set balance = balance + ? where id = ? and user_id = ?").Bind(transfer, toId, toUserId).Exec()
if err != nil {
return err
}
return tx.Commit()
}

View File

@@ -14,14 +14,12 @@ 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.
@@ -29,8 +27,7 @@ type Task struct {
ID int `json:"id"` ID int `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Reward float64 `json:"reward"` Reward float64 `json:"reward"`
Assigned *int `json:"assigned"` Assigned *int `json:"assigned"` // Pointer to allow null
Schedule *string `json:"schedule"`
} }
type Allowance struct { type Allowance struct {
@@ -69,20 +66,8 @@ type CreateTaskRequest struct {
Name string `json:"name" binding:"required"` Name string `json:"name" binding:"required"`
Reward float64 `json:"reward"` Reward float64 `json:"reward"`
Assigned *int `json:"assigned"` Assigned *int `json:"assigned"`
Schedule *string `json:"schedule"`
} }
type CreateTaskResponse struct { type CreateTaskResponse struct {
ID int `json:"id"` ID int `json:"id"`
} }
type AddAllowanceAmountRequest struct {
Amount float64 `json:"amount"`
Description string `json:"description"`
}
type TransferRequest struct {
From int `json:"from"`
To int `json:"to"`
Amount float64 `json:"amount"`
}

View File

@@ -3,34 +3,30 @@ module allowance_planner
go 1.24.2 go 1.24.2
require ( require (
gitea.seeseepuff.be/seeseemelk/mysqlite v0.15.0 gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0
github.com/adhocore/gronx v1.19.6
github.com/gavv/httpexpect/v2 v2.17.0 github.com/gavv/httpexpect/v2 v2.17.0
github.com/gin-contrib/cors v1.7.6 github.com/gin-contrib/cors v1.7.5
github.com/gin-gonic/gin v1.11.0 github.com/gin-gonic/gin v1.10.0
github.com/stretchr/testify v1.11.1
) )
require ( require (
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect
github.com/ajg/form v1.5.1 // indirect github.com/ajg/form v1.5.1 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/brotli v1.1.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic v1.14.1 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.18.0 // indirect github.com/fatih/color v1.18.0 // indirect
github.com/fatih/structs v1.1.0 // indirect github.com/fatih/structs v1.1.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.28.0 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/gorilla/websocket v1.5.3 // indirect
@@ -38,49 +34,44 @@ require (
github.com/imkira/go-interpol v1.1.0 // indirect github.com/imkira/go-interpol v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.55.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sanity-io/litter v1.5.8 // indirect github.com/sanity-io/litter v1.5.8 // indirect
github.com/sergi/go-diff v1.4.0 // indirect github.com/sergi/go-diff v1.3.1 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.67.0 // indirect github.com/valyala/fasthttp v1.62.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect
github.com/yudai/gojsondiff v1.0.0 // indirect github.com/yudai/gojsondiff v1.0.0 // indirect
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
go.uber.org/mock v0.6.0 // indirect golang.org/x/arch v0.17.0 // indirect
golang.org/x/arch v0.22.0 // indirect golang.org/x/crypto v0.38.0 // indirect
golang.org/x/crypto v0.42.0 // indirect golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect golang.org/x/net v0.40.0 // indirect
golang.org/x/mod v0.29.0 // indirect golang.org/x/sys v0.33.0 // indirect
golang.org/x/net v0.45.0 // indirect golang.org/x/text v0.25.0 // indirect
golang.org/x/sync v0.17.0 // indirect google.golang.org/protobuf v1.36.6 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/tools v0.37.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.66.10 // indirect modernc.org/libc v1.65.7 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.39.0 // indirect modernc.org/sqlite v1.37.0 // indirect
moul.io/http2curl/v2 v2.3.0 // indirect moul.io/http2curl/v2 v2.3.0 // indirect
zombiezen.com/go/sqlite v1.4.2 // indirect zombiezen.com/go/sqlite v1.4.0 // indirect
) )

View File

@@ -1,21 +1,19 @@
gitea.seeseepuff.be/seeseemelk/mysqlite v0.15.0 h1:+k0iBYM/aZJxz7++EKi/G9e66E9u4bPS3DFLrBeDb9Y= gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0 h1:aRItVfUj48fBmuec7rm/jY9KCfvHW2VzJfItVk4t8sw=
gitea.seeseepuff.be/seeseemelk/mysqlite v0.15.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE= gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4= github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4=
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w=
github.com/adhocore/gronx v1.19.6 h1:5KNVcoR9ACgL9HhEqCm5QXsab/gI4QDIybTAWcXDKDc=
github.com/adhocore/gronx v1.19.6/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -26,33 +24,31 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gavv/httpexpect/v2 v2.17.0 h1:nIJqt5v5e4P7/0jODpX2gtSw+pHXUqdP28YcjqwDZmE= github.com/gavv/httpexpect/v2 v2.17.0 h1:nIJqt5v5e4P7/0jODpX2gtSw+pHXUqdP28YcjqwDZmE=
github.com/gavv/httpexpect/v2 v2.17.0/go.mod h1:E8ENFlT9MZ3Si2sfM6c6ONdwXV2noBCGkhA+lkJgkP0= github.com/gavv/httpexpect/v2 v2.17.0/go.mod h1:E8ENFlT9MZ3Si2sfM6c6ONdwXV2noBCGkhA+lkJgkP0=
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= github.com/gin-contrib/cors v1.7.5 h1:cXC9SmofOrRg0w9PigwGlHG3ztswH6bqq4vJVXnvYMk=
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= github.com/gin-contrib/cors v1.7.5/go.mod h1:4q3yi7xBEDDWKapjT2o1V7mScKDDr8k+jZ0fSquGoy0=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -70,8 +66,10 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
@@ -91,8 +89,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
@@ -103,10 +101,6 @@ github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnI
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
@@ -114,28 +108,29 @@ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6po
github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg= github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg=
github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.67.0 h1:tqKlJMUP6iuNG8hGjK/s9J4kadH7HLV4ijEcPGsezac= github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0=
github.com/valyala/fasthttp v1.67.0/go.mod h1:qYSIpqt/0XNmShgo/8Aq8E3UYWVVwNS2QYmzd8WIEPM= github.com/valyala/fasthttp v1.62.0/go.mod h1:FCINgr4GKdKqV8Q0xv8b+UxPV+H/O5nNFo3D+r54Htg=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@@ -154,51 +149,49 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDf
github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI= github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI=
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -213,18 +206,16 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -233,13 +224,14 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY= modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs=
moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE=
zombiezen.com/go/sqlite v1.4.2 h1:KZXLrBuJ7tKNEm+VJcApLMeQbhmAUOKA5VWS93DfFRo= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
zombiezen.com/go/sqlite v1.4.2/go.mod h1:5Kd4taTAD4MkBzT25mQ9uaAlLjyR0rFhsR6iINO70jc= zombiezen.com/go/sqlite v1.4.0 h1:N1s3RIljwtp4541Y8rM880qgGIgq3fTD2yks1xftnKU=
zombiezen.com/go/sqlite v1.4.0/go.mod h1:0w9F1DN9IZj9AcLS9YDKMboubCACkwYCGkzoy3eG5ik=

View File

@@ -4,14 +4,13 @@ import (
"context" "context"
"embed" "embed"
"errors" "errors"
"gitea.seeseepuff.be/seeseemelk/mysqlite"
"log" "log"
"net" "net"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"gitea.seeseepuff.be/seeseemelk/mysqlite"
"github.com/gin-contrib/cors" "github.com/gin-contrib/cors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -25,8 +24,6 @@ const (
ErrInvalidUserID = "Invalid user ID" ErrInvalidUserID = "Invalid user ID"
ErrUserNotFound = "User not found" ErrUserNotFound = "User not found"
ErrCheckingUserExist = "Error checking user existence: %v" ErrCheckingUserExist = "Error checking user existence: %v"
ErrInsufficientFunds = "Insufficient funds in source allowance"
ErrDifferentUsers = "Allowances do not belong to the same user"
) )
// ServerConfig holds configuration for the server. // ServerConfig holds configuration for the server.
@@ -376,56 +373,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 {
@@ -439,11 +386,6 @@ func createTask(c *gin.Context) {
return return
} }
if taskRequest.Schedule != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Schedules are not yet supported"})
return
}
// If assigned is not nil, check if user exists // If assigned is not nil, check if user exists
if taskRequest.Assigned != nil { if taskRequest.Assigned != nil {
exists, err := db.UserExists(*taskRequest.Assigned) exists, err := db.UserExists(*taskRequest.Assigned)
@@ -521,11 +463,6 @@ func putTask(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"}) c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
return return
} }
if err != nil {
log.Printf("Error getting task: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
err = db.UpdateTask(taskId, &taskRequest) err = db.UpdateTask(taskId, &taskRequest)
if err != nil { if err != nil {
@@ -607,11 +544,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)
@@ -651,32 +583,6 @@ func getHistory(c *gin.Context) {
c.IndentedJSON(http.StatusOK, history) c.IndentedJSON(http.StatusOK, history)
} }
func transfer(c *gin.Context) {
var transferRequest TransferRequest
if err := c.ShouldBindJSON(&transferRequest); err != nil {
log.Printf("Error parsing request body: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
err := db.TransferAllowance(transferRequest.From, transferRequest.To, transferRequest.Amount)
if err != nil {
if errors.Is(err, mysqlite.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "Allowance not found"})
return
}
if err.Error() == ErrInsufficientFunds || err.Error() == ErrDifferentUsers {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
log.Printf("Error transferring allowance: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Transfer successful"})
}
/* /*
Initialises the database, and then starts the server. Initialises the database, and then starts the server.
If the context gets cancelled, the server is shutdown and the database is closed. If the context gets cancelled, the server is shutdown and the database is closed.
@@ -705,14 +611,12 @@ 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)
router.PUT("/api/task/:taskId", putTask) router.PUT("/api/task/:taskId", putTask)
router.DELETE("/api/task/:taskId", deleteTask) router.DELETE("/api/task/:taskId", deleteTask)
router.POST("/api/task/:taskId/complete", completeTask) router.POST("/api/task/:taskId/complete", completeTask)
router.POST("/api/transfer", transfer)
srv := &http.Server{ srv := &http.Server{
Addr: config.Addr, Addr: config.Addr,

View File

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

View File

@@ -1,3 +0,0 @@
alter table tasks add column schedule text;
alter table tasks add column completed date;
alter table tasks add column next_run date;

View File

@@ -28,13 +28,8 @@ func loadWebEndpoints(router *gin.Engine) {
} }
func redirectToPage(c *gin.Context, page string) { func redirectToPage(c *gin.Context, page string) {
redirectToPageStatus(c, page, http.StatusSeeOther)
}
func redirectToPageStatus(c *gin.Context, page string, status int) {
scheme := c.Request.URL.Scheme scheme := c.Request.URL.Scheme
target := scheme + domain + page c.Redirect(http.StatusSeeOther, scheme+domain+page)
c.Redirect(status, target)
} }
func renderLogin(c *gin.Context) { func renderLogin(c *gin.Context) {
@@ -71,24 +66,16 @@ func renderCreateTask(c *gin.Context) {
return return
} }
request := &CreateTaskRequest{ _, err = db.CreateTask(&CreateTaskRequest{
Name: name, Name: name,
Reward: reward, Reward: reward,
} })
schedule := c.PostForm("schedule")
if schedule != "" {
request.Schedule = &schedule
}
_, err = db.CreateTask(request)
if err != nil { if err != nil {
renderError(c, http.StatusInternalServerError, err) renderError(c, http.StatusInternalServerError, err)
return return
} }
redirectToPageStatus(c, "/", http.StatusFound) c.Redirect(http.StatusFound, "/")
} }
func renderCompleteTask(c *gin.Context) { func renderCompleteTask(c *gin.Context) {
@@ -105,7 +92,7 @@ func renderCompleteTask(c *gin.Context) {
return return
} }
redirectToPageStatus(c, "/", http.StatusFound) c.Redirect(http.StatusFound, "/")
} }
func renderCreateAllowance(c *gin.Context) { func renderCreateAllowance(c *gin.Context) {
@@ -142,7 +129,7 @@ func renderCreateAllowance(c *gin.Context) {
return return
} }
redirectToPageStatus(c, "/", http.StatusFound) c.Redirect(http.StatusFound, "/")
} }
func renderCompleteAllowance(c *gin.Context) { func renderCompleteAllowance(c *gin.Context) {
@@ -164,7 +151,7 @@ func renderCompleteAllowance(c *gin.Context) {
return return
} }
redirectToPageStatus(c, "/", http.StatusFound) c.Redirect(http.StatusFound, "/")
} }
func getCurrentUser(c *gin.Context) *int { func getCurrentUser(c *gin.Context) *int {
@@ -193,7 +180,7 @@ func getCurrentUser(c *gin.Context) *int {
func unsetUserCookie(c *gin.Context) { func unsetUserCookie(c *gin.Context) {
c.SetCookie("user", "", -1, "/", "localhost", false, true) c.SetCookie("user", "", -1, "/", "localhost", false, true)
redirectToPageStatus(c, "/", http.StatusFound) c.Redirect(http.StatusFound, "/")
} }
func renderNoUser(c *gin.Context) { func renderNoUser(c *gin.Context) {

View File

@@ -3,11 +3,9 @@
<head> <head>
<title>Allowance Planner 2000</title> <title>Allowance Planner 2000</title>
<style> <style>
<!--
tr:hover { tr:hover {
background-color: #f0f0f0; background-color: #f0f0f0;
} }
-->
</style> </style>
</head> </head>
<body> <body>
@@ -29,7 +27,7 @@
{{if ne .CurrentUser 0}} {{if ne .CurrentUser 0}}
<h2>Allowances</h2> <h2>Allowances</h2>
<form action="/createAllowance" method="post"> <form action="/createAllowance" method="post">
<table border=1> <table border="1">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
@@ -45,7 +43,7 @@
<td></td> <td></td>
<td><label><input type="number" name="target" placeholder="Target"></label></td> <td><label><input type="number" name="target" placeholder="Target"></label></td>
<td><label><input type="number" name="weight" placeholder="Weight"></label></td> <td><label><input type="number" name="weight" placeholder="Weight"></label></td>
<td><input type="submit" value="Create"></td> <td><button>Create</button></td>
</tr> </tr>
{{range .Allowances}} {{range .Allowances}}
{{if eq .ID 0}} {{if eq .ID 0}}
@@ -81,7 +79,6 @@
<th>Name</th> <th>Name</th>
<th>Assigned</th> <th>Assigned</th>
<th>Reward</th> <th>Reward</th>
<th>Schedule</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
@@ -97,7 +94,6 @@
{{end}} {{end}}
</td> </td>
<td>{{.Reward}}</td> <td>{{.Reward}}</td>
<td>{{.Schedule}}</td>
<td> <td>
<a href="/completeTask?task={{.ID}}">Mark as completed</a> <a href="/completeTask?task={{.ID}}">Mark as completed</a>
</td> </td>
@@ -107,8 +103,7 @@
<td><label><input type="text" name="name" placeholder="Name"></label></td> <td><label><input type="text" name="name" placeholder="Name"></label></td>
<td></td> <td></td>
<td><label><input type="number" name="reward" placeholder="Reward"></label></td> <td><label><input type="number" name="reward" placeholder="Reward"></label></td>
<td><label><input type="text" name="schedule" placeholder="Schedule"></label></td> <td><button>Create</button></td>
<td><input type="submit" value="Create"></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -409,59 +409,6 @@ paths:
404: 404:
description: The task could not be found. description: The task could not be found.
/api/transfer:
post:
summary: Transfer amount between allowances
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
from:
type: integer
description: Source allowance ID
to:
type: integer
description: Destination allowance ID
amount:
type: number
format: float
description: Amount to transfer
required:
- from
- to
- amount
responses:
'200':
description: Transfer successful
content:
application/json:
schema:
type: object
properties:
message:
type: string
'400':
description: Invalid request
content:
application/json:
schema:
type: object
properties:
error:
type: string
'404':
description: Allowance not found
content:
application/json:
schema:
type: object
properties:
error:
type: string
components: components:
schemas: schemas:
task: task:
@@ -475,10 +422,7 @@ components:
description: The task name description: The task name
reward: reward:
type: integer type: integer
description: The task reward description: The task reward, in cents
schedule:
type: string
description: The schedule of the task, in cron format
assigned: assigned:
type: integer type: integer
description: The user ID of the user assigned to the task description: The user ID of the user assigned to the task

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<resources> <resources>
<string name="app_name">Allowance Planner V2</string> <string name="app_name">allowance-planner-v2</string>
<string name="title_activity_main">Allowance Planner V2</string> <string name="title_activity_main">allowance-planner-v2</string>
<string name="package_name">io.ionic.starter</string> <string name="package_name">io.ionic.starter</string>
<string name="custom_url_scheme">io.ionic.starter</string> <string name="custom_url_scheme">io.ionic.starter</string>
</resources> </resources>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -2,7 +2,7 @@ import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = { const config: CapacitorConfig = {
appId: 'io.ionic.starter', appId: 'io.ionic.starter',
appName: 'Allowance Planner V2', appName: 'allowance-planner-v2',
webDir: 'www' webDir: 'www'
}; };

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,23 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { AddAllowancePageRoutingModule } from './add-allowance-routing.module';
import { AddAllowancePage } from './add-allowance.page';
import { MatIconModule } from '@angular/material/icon';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
AddAllowancePageRoutingModule,
ReactiveFormsModule,
MatIconModule
],
declarations: [AddAllowancePage]
})
export class AddAllowancePageModule {}

View File

@@ -1,27 +0,0 @@
<ion-header [translucent]="true">
<ion-toolbar>
<div class="toolbar">
<div class="icon" (click)="navigateBack()">
<mat-icon>arrow_back</mat-icon>
</div>
<ion-title *ngIf="isAddMode && goalId == 0">Add to Allowance</ion-title>
<ion-title *ngIf="isAddMode && goalId != 0">Add to Goal</ion-title>
<ion-title *ngIf="!isAddMode">Spend Allowance</ion-title>
</div>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<form [formGroup]="form">
<label>Amount</label>
<input id="amount" type="number" placeholder="0.00" name="price" min="0" value="0" step="0.01" formControlName="amount"/>
<label>Description</label>
<input id="description" type="text" formControlName="description"/>
<button type="button" [disabled]="!form.valid" (click)="changeAllowance()">
<span *ngIf="isAddMode">Add</span>
<span *ngIf="!isAddMode">Spend</span>
</button>
</form>
</ion-content>

View File

@@ -1,49 +0,0 @@
.toolbar {
display: flex;
align-items: center;
}
.icon {
margin-left: 5px;
}
form {
height: 100%;
}
form,
.item {
display: flex;
flex-direction: column;
align-items: center;
}
input {
border: 1px solid var(--ion-color-primary);
border-radius: 5px;
width: 250px;
height: 40px;
padding-inline: 10px;
display: flex;
align-items: center;
}
label {
color: var(--ion-color-primary);
margin-top: 25px;
margin-bottom: 10px;
}
button {
background-color: var(--ion-color-primary);
border-radius: 5px;
color: white;
padding: 10px;
width: 250px;
margin-top: 100px;
}
button:disabled,
button[disabled]{
opacity: 0.5;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
} }
@@ -126,14 +127,4 @@ button[disabled] {
background-color: var(--legend-color); background-color: var(--legend-color);
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;
} }

View File

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

View File

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

View File

@@ -1,25 +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';
import { MatSelectModule } from '@angular/material/select';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
EditAllowancePageRoutingModule,
ReactiveFormsModule,
MatIconModule,
MatSelectModule
],
declarations: [EditAllowancePage]
})
export class EditAllowancePageModule {}

View File

@@ -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>
</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>
<mat-select [(value)]="selectedColor" formControlName="color" [style.--color]="selectedColor">
<mat-option *ngFor="let color of possibleColors" [value]="color" [style.--background]="color">{{color}}</mat-option>
</mat-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>
<button
*ngIf="!isAddMode && goalId !=0"
class="remove-button"
(click)="deleteAllowance()"
>Delete Goal</button>
</form>
</ion-content>

View File

@@ -1,64 +0,0 @@
.toolbar {
display: flex;
align-items: center;
}
.remove-button {
margin-top: 10px;
background-color: var(--negative-amount-color);
}
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,
mat-select {
--color: black;
color: var(--color);
border: 1px solid var(--ion-color-primary);
border-radius: 5px;
width: 250px;
height: 40px;
padding-inline: 10px;
display: flex;
align-items: center;
font-family: (--ion-font-family);
}
mat-option {
--background: white;
color: var(--background);
font-family: (--ion-font-family);
}
button {
background-color: var(--ion-color-primary);
border-radius: 5px;
color: white;
padding: 10px;
width: 250px;
margin-top: 100px;
}
button:disabled,
button[disabled]{
opacity: 0.5;
}
.icon {
margin-left: 5px;
}

View File

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

View File

@@ -1,109 +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 selectedColor: string = '';
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
});
this.selectedColor = this.form.value.color;
}
});
}
}
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();
}
}

View File

@@ -7,8 +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';
import { MatSelectModule } from '@angular/material/select';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -16,9 +14,7 @@ import { MatSelectModule } from '@angular/material/select';
FormsModule, FormsModule,
IonicModule, IonicModule,
EditTaskPageRoutingModule, EditTaskPageRoutingModule,
ReactiveFormsModule, ReactiveFormsModule
MatIconModule,
MatSelectModule
], ],
declarations: [EditTaskPage] declarations: [EditTaskPage]
}) })

View File

@@ -1,12 +1,13 @@
<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 class="done-button" *ngIf="!isAddMode" (click)="completeAndRecreateTask()">Done & Re-create</button> <button
*ngIf="!isAddMode"
class="remove-button"
(click)="deleteTask()"
>Delete task</button>
</div> </div>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
@@ -17,21 +18,16 @@
<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>
<mat-select formControlName="assigned"> <select formControlName="assigned">
<mat-option *ngFor="let user of users" [value]="user.id">{{ user.name }}</mat-option> <option *ngFor="let user of users" [value]="user.id">{{ user.name }}</option>
</mat-select> </select>
<button type="button" [disabled]="!form.valid" (click)="submit()"> <button type="button" [disabled]="!form.valid" (click)="submit()">
<span *ngIf="isAddMode">Add Task</span> <span *ngIf="isAddMode">Add Task</span>
<span *ngIf="!isAddMode">Update Task</span> <span *ngIf="!isAddMode">Update Task</span>
</button> </button>
<button
*ngIf="!isAddMode"
class="remove-button"
(click)="deleteTask()"
>Delete task</button>
</form> </form>
</ion-content> </ion-content>

View File

@@ -1,11 +1,12 @@
.toolbar { .toolbar {
display: flex; display: flex;
align-items: center;
} }
.remove-button { .remove-button {
margin-top: 10px; background-color: var(--ion-color-primary);
background-color: var(--negative-amount-color); margin-right: 15px;
width: 85px;
margin-bottom: 0;
} }
form { form {
@@ -22,15 +23,10 @@ label {
} }
input, input,
mat-select { select {
border: 1px solid var(--ion-color-primary); border: 1px solid var(--ion-color-primary);
border-radius: 5px; border-radius: 5px;
width: 250px; width: 250px;
height: 40px;
padding-inline: 10px;
display: flex;
align-items: center;
font-family: (--ion-font-family);
} }
button { button {
@@ -39,21 +35,11 @@ button {
color: white; color: white;
padding: 10px; padding: 10px;
width: 250px; width: 250px;
margin-top: 100px; margin-top: auto;
} margin-bottom: 50px;
.done-button {
width: 150px;
margin-top: unset;
margin-right: 20px;
border-radius: 10px;
} }
button:disabled, button:disabled,
button[disabled]{ button[disabled]{
opacity: 0.5; opacity: 0.5;
}
.icon {
margin-left: 5px;
} }

View File

@@ -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;
@@ -57,13 +56,13 @@ export class EditTaskPage implements OnInit {
let assigned: number | null = Number(formValue.assigned); let assigned: number | null = Number(formValue.assigned);
if (assigned === 0) { if (assigned === 0) {
assigned = null; assigned = null;
}; }
const task = { const task = {
name: formValue.name, name: formValue.name,
reward: formValue.reward, reward: formValue.reward,
assigned assigned
}; }
if (this.isAddMode) { if (this.isAddMode) {
this.taskService.createTask(task); this.taskService.createTask(task);
@@ -78,27 +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']);
} }
completeAndRecreateTask() {
const formValue = this.form.value;
let assigned: number | null = Number(formValue.assigned);
if (assigned === 0) {
assigned = null;
};
const task = {
name: formValue.name,
reward: formValue.reward,
assigned
};
this.taskService.createTask(task);
this.taskService.completeTask(this.id);
this.router.navigate(['/tabs/tasks']);
}
navigateBack() {
this.location.back();
}
} }

View File

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

View File

@@ -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.toFixed(2) }} SP</div>
</div>
</ion-content> </ion-content>

View File

@@ -1,30 +0,0 @@
.item {
display: flex;
flex-direction: row;
align-items: center;
border-bottom: 1px solid var(--line-color);
padding: 8px;
}
.left {
width: 70%;
font-size: 18px;
}
.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);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,19 +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="text"> <div class="name">{{ task.name }}</div>
<div class="name">
{{ task.name }}
<span class="assigned">{{ usernames[task.assigned ? task.assigned : 0] }}</span>
</div>
</div>
<div <div
class="reward" class="reward"
[ngClass]="{ 'negative': task.reward < 0 }" [ngClass]="{ 'negative': task.reward < 0 }"

View File

@@ -31,8 +31,6 @@ mat-icon {
align-items: center; align-items: center;
border-bottom: 1px solid var(--line-color); border-bottom: 1px solid var(--line-color);
padding: 5px; padding: 5px;
padding-block: 10px;
font-size: 18px;
} }
.item { .item {
@@ -43,6 +41,7 @@ mat-icon {
} }
.name { .name {
margin-left: 10px;
color: var(--font-color); color: var(--font-color);
} }
@@ -50,7 +49,6 @@ mat-icon {
margin-left: auto; margin-left: auto;
margin-right: 15px; margin-right: 15px;
color: var(--positive-amount-color); color: var(--positive-amount-color);
font-size: 22px;
} }
.negative { .negative {
@@ -58,28 +56,15 @@ mat-icon {
} }
button { button {
height: 45px; width: 57px;
height: 30px;
border-radius: 10px; border-radius: 10px;
color: white; color: white;
background: var(--confirm-button-color); background: var(--confirm-button-color);
padding-inline: 15px;
} }
.add-button { .add-button {
background-color: var(--ion-color-primary); background-color: var(--ion-color-primary);
margin-right: 15px; margin-right: 15px;
height: 30px; width: 75px;
}
.assigned {
color: var(--line-color);
margin-left: 3px;
font-size: 15px;
}
.text {
display: flex;
align-items: center;
width: 60%;
margin-left: 10px;
} }

View File

@@ -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);
}); });
}, 100); }, 10);
} }
createTask() { createTask() {

View File

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

View File

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

View File

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

View File

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

View File

@@ -38,34 +38,8 @@
ion-title { ion-title {
color: var(--ion-color-primary); color: var(--ion-color-primary);
font-size: 24px;
} }
ion-header { ion-header {
border-bottom: 1px solid var(--line-color); border-bottom: 1px solid var(--line-color);
}
button {
font-size: 16px;
}
ion-header.md {
ion-toolbar:first-child {
--padding-top: 30px;
--padding-bottom: 15px;
}
}
label {
font-size: 18px;
}
ion-alert .alert-wrapper.sc-ion-alert-md {
background-color: var(--ion-background-color) !important;
--background: unset !important;
box-shadow: unset;
}
ion-alert .alert-tappable.sc-ion-alert-md {
background-color: var(--test-color);
} }