Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
46a4bfcd27 | |||
efc2453243 | |||
2e81a635ee |
1
backend/.gitignore
vendored
@ -1,4 +1,3 @@
|
|||||||
*.db3
|
*.db3
|
||||||
*.db3-*
|
*.db3-*
|
||||||
*.db3.*
|
|
||||||
/allowance_planner
|
/allowance_planner
|
||||||
|
BIN
backend/allowance_planner.db3.backup.3
Normal file
@ -15,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
|
||||||
@ -285,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)
|
||||||
|
|
||||||
|
102
backend/db.go
@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/adhocore/gronx"
|
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"time"
|
"time"
|
||||||
@ -314,20 +313,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 +340,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 +362,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 {
|
||||||
@ -530,10 +453,7 @@ 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()
|
||||||
}
|
}
|
||||||
|
@ -29,8 +29,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,7 +68,6 @@ 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 {
|
||||||
|
@ -6,12 +6,11 @@ require (
|
|||||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0
|
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0
|
||||||
github.com/gavv/httpexpect/v2 v2.17.0
|
github.com/gavv/httpexpect/v2 v2.17.0
|
||||||
github.com/gin-contrib/cors v1.7.5
|
github.com/gin-contrib/cors v1.7.5
|
||||||
github.com/gin-gonic/gin v1.10.1
|
github.com/gin-gonic/gin v1.10.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect
|
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect
|
||||||
github.com/adhocore/gronx v1.19.6 // indirect
|
|
||||||
github.com/ajg/form v1.5.1 // indirect
|
github.com/ajg/form v1.5.1 // indirect
|
||||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||||
github.com/bytedance/sonic v1.13.2 // indirect
|
github.com/bytedance/sonic v1.13.2 // indirect
|
||||||
@ -50,7 +49,7 @@ require (
|
|||||||
github.com/sergi/go-diff v1.3.1 // indirect
|
github.com/sergi/go-diff v1.3.1 // indirect
|
||||||
github.com/stretchr/testify v1.10.0 // 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.2.14 // 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.62.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
|
||||||
@ -69,10 +68,10 @@ require (
|
|||||||
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.65.8 // 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.37.1 // 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
|
||||||
)
|
)
|
||||||
|
@ -2,8 +2,6 @@ gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0 h1:aRItVfUj48fBmuec7rm/jY9KCfvHW
|
|||||||
gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.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.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||||
@ -36,8 +34,6 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
|
|||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
|
||||||
github.com/gin-gonic/gin v1.10.1/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=
|
||||||
@ -131,8 +127,6 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|||||||
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.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
github.com/ugorji/go/codec v1.2.14 h1:yOQvXCBc3Ij46LRkRoh4Yd5qK6LVOgi0bYOXfb7ifjw=
|
|
||||||
github.com/ugorji/go/codec v1.2.14/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.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0=
|
github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0=
|
||||||
@ -222,8 +216,6 @@ modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
|||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
|
modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
|
||||||
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
|
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
|
||||||
modernc.org/libc v1.65.8 h1:7PXRJai0TXZ8uNA3srsmYzmTyrLoHImV5QxHeni108Q=
|
|
||||||
modernc.org/libc v1.65.8/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
|
|
||||||
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=
|
||||||
@ -234,8 +226,6 @@ 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.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
|
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
|
||||||
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
|
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
|
||||||
modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs=
|
|
||||||
modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g=
|
|
||||||
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=
|
||||||
@ -245,5 +235,3 @@ moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHc
|
|||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
zombiezen.com/go/sqlite v1.4.0 h1:N1s3RIljwtp4541Y8rM880qgGIgq3fTD2yks1xftnKU=
|
zombiezen.com/go/sqlite v1.4.0 h1:N1s3RIljwtp4541Y8rM880qgGIgq3fTD2yks1xftnKU=
|
||||||
zombiezen.com/go/sqlite v1.4.0/go.mod h1:0w9F1DN9IZj9AcLS9YDKMboubCACkwYCGkzoy3eG5ik=
|
zombiezen.com/go/sqlite v1.4.0/go.mod h1:0w9F1DN9IZj9AcLS9YDKMboubCACkwYCGkzoy3eG5ik=
|
||||||
zombiezen.com/go/sqlite v1.4.2 h1:KZXLrBuJ7tKNEm+VJcApLMeQbhmAUOKA5VWS93DfFRo=
|
|
||||||
zombiezen.com/go/sqlite v1.4.2/go.mod h1:5Kd4taTAD4MkBzT25mQ9uaAlLjyR0rFhsR6iINO70jc=
|
|
||||||
|
@ -4,9 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"embed"
|
"embed"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"gitea.seeseepuff.be/seeseemelk/mysqlite"
|
"gitea.seeseepuff.be/seeseemelk/mysqlite"
|
||||||
"github.com/adhocore/gronx"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -45,11 +43,6 @@ type ServerConfig struct {
|
|||||||
Started chan bool
|
Started chan bool
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultDomain = "localhost:8080"
|
|
||||||
|
|
||||||
// The domain that the server is reachable at.
|
|
||||||
var domain = DefaultDomain
|
|
||||||
|
|
||||||
func getUsers(c *gin.Context) {
|
func getUsers(c *gin.Context) {
|
||||||
users, err := db.GetUsers()
|
users, err := db.GetUsers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -438,14 +431,6 @@ func createTask(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if taskRequest.Schedule != nil {
|
|
||||||
valid := gronx.IsValid(*taskRequest.Schedule)
|
|
||||||
if !valid {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid cron schedule: %s", *taskRequest.Schedule)})
|
|
||||||
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)
|
||||||
@ -523,11 +508,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 {
|
||||||
@ -726,10 +706,5 @@ func main() {
|
|||||||
config.Datasource = "allowance_planner.db3"
|
config.Datasource = "allowance_planner.db3"
|
||||||
log.Printf("Warning: No DB_PATH set, using default of %s", config.Datasource)
|
log.Printf("Warning: No DB_PATH set, using default of %s", config.Datasource)
|
||||||
}
|
}
|
||||||
domain = os.Getenv("DOMAIN")
|
|
||||||
if domain == "" {
|
|
||||||
domain = DefaultDomain
|
|
||||||
log.Printf("Warning: No DOMAIN set, using default of %s", domain)
|
|
||||||
}
|
|
||||||
start(context.Background(), &config)
|
start(context.Background(), &config)
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
@ -27,22 +26,11 @@ func loadWebEndpoints(router *gin.Engine) {
|
|||||||
router.GET("/completeAllowance", renderCompleteAllowance)
|
router.GET("/completeAllowance", renderCompleteAllowance)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
target := scheme + domain + page
|
|
||||||
c.Redirect(status, target)
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderLogin(c *gin.Context) {
|
func renderLogin(c *gin.Context) {
|
||||||
if c.Query("user") != "" {
|
if c.Query("user") != "" {
|
||||||
log.Println("Set cookie for user:", c.Query("user"))
|
c.SetCookie("user", c.Query("user"), 3600, "/", "localhost", false, true)
|
||||||
c.SetCookie("user", c.Query("user"), 3600, "", "", false, true)
|
|
||||||
}
|
}
|
||||||
redirectToPage(c, "/")
|
c.Redirect(http.StatusFound, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderIndex(c *gin.Context) {
|
func renderIndex(c *gin.Context) {
|
||||||
@ -71,24 +59,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 +85,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 +122,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,12 +144,11 @@ 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 {
|
||||||
currentUserStr, err := c.Cookie("user")
|
currentUserStr, err := c.Cookie("user")
|
||||||
log.Println("Cookie string:", currentUserStr)
|
|
||||||
if errors.Is(err, http.ErrNoCookie) {
|
if errors.Is(err, http.ErrNoCookie) {
|
||||||
renderNoUser(c)
|
renderNoUser(c)
|
||||||
return nil
|
return nil
|
||||||
@ -193,7 +172,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) {
|
||||||
|
@ -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>
|
||||||
|
@ -422,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
|
||||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 9.0 KiB |
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 3.9 KiB |
@ -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>
|
@ -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>
|
Before Width: | Height: | Size: 660 B |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 296 B |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 408 B |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1006 B |
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 15 KiB |
@ -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>
|
||||||
|
Before Width: | Height: | Size: 130 KiB |
@ -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'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,16 +18,6 @@ form,
|
|||||||
align-items: center;
|
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 {
|
label {
|
||||||
color: var(--ion-color-primary);
|
color: var(--ion-color-primary);
|
||||||
margin-top: 25px;
|
margin-top: 25px;
|
||||||
@ -40,7 +30,8 @@ button {
|
|||||||
color: white;
|
color: white;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
width: 250px;
|
width: 250px;
|
||||||
margin-top: 100px;
|
margin-top: auto;
|
||||||
|
margin-bottom: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled,
|
button:disabled,
|
||||||
|
@ -36,7 +36,7 @@ export class AllowancePage implements ViewWillEnter {
|
|||||||
allowance[0].name = 'Main Allowance';
|
allowance[0].name = 'Main Allowance';
|
||||||
this.allowance$.next(allowance);
|
this.allowance$.next(allowance);
|
||||||
})
|
})
|
||||||
}, 100);
|
}, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
canFinishGoal(allowance: Allowance): boolean {
|
canFinishGoal(allowance: Allowance): boolean {
|
||||||
|
@ -8,7 +8,6 @@ import { EditAllowancePageRoutingModule } from './edit-allowance-routing.module'
|
|||||||
|
|
||||||
import { EditAllowancePage } from './edit-allowance.page';
|
import { EditAllowancePage } from './edit-allowance.page';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@ -17,8 +16,7 @@ import { MatSelectModule } from '@angular/material/select';
|
|||||||
IonicModule,
|
IonicModule,
|
||||||
EditAllowancePageRoutingModule,
|
EditAllowancePageRoutingModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
MatIconModule,
|
MatIconModule
|
||||||
MatSelectModule
|
|
||||||
],
|
],
|
||||||
declarations: [EditAllowancePage]
|
declarations: [EditAllowancePage]
|
||||||
})
|
})
|
||||||
|
@ -7,6 +7,11 @@
|
|||||||
<ion-title *ngIf="isAddMode">Create Goal</ion-title>
|
<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 Goal</ion-title>
|
||||||
<ion-title *ngIf="!isAddMode && goalId == 0">Edit Allowance</ion-title>
|
<ion-title *ngIf="!isAddMode && goalId == 0">Edit Allowance</ion-title>
|
||||||
|
<button
|
||||||
|
*ngIf="!isAddMode && goalId !=0"
|
||||||
|
class="remove-button"
|
||||||
|
(click)="deleteAllowance()"
|
||||||
|
>Delete Goal</button>
|
||||||
</div>
|
</div>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
@ -28,9 +33,9 @@
|
|||||||
|
|
||||||
<div class="item" *ngIf="isAddMode || goalId != 0">
|
<div class="item" *ngIf="isAddMode || goalId != 0">
|
||||||
<label>Colour</label>
|
<label>Colour</label>
|
||||||
<mat-select [(value)]="selectedColor" formControlName="color" [style.--color]="selectedColor">
|
<select formControlName="color">
|
||||||
<mat-option *ngFor="let color of possibleColors" [value]="color" [style.--background]="color">{{color}}</mat-option>
|
<option *ngFor="let color of possibleColors" [value]="color" [style.--background]="color">{{color}}</option>
|
||||||
</mat-select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" [disabled]="!form.valid" (click)="submit()">
|
<button type="button" [disabled]="!form.valid" (click)="submit()">
|
||||||
@ -38,10 +43,5 @@
|
|||||||
<span *ngIf="!isAddMode && goalId != 0">Update Goal</span>
|
<span *ngIf="!isAddMode && goalId != 0">Update Goal</span>
|
||||||
<span *ngIf="!isAddMode && goalId == 0">Update Allowance</span>
|
<span *ngIf="!isAddMode && goalId == 0">Update Allowance</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
*ngIf="!isAddMode && goalId !=0"
|
|
||||||
class="remove-button"
|
|
||||||
(click)="deleteAllowance()"
|
|
||||||
>Delete Goal</button>
|
|
||||||
</form>
|
</form>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
@ -4,8 +4,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.remove-button {
|
.remove-button {
|
||||||
margin-top: 10px;
|
background-color: var(--ion-color-primary);
|
||||||
background-color: var(--negative-amount-color);
|
margin-right: 15px;
|
||||||
|
width: 100px;
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
@ -26,23 +28,17 @@ label {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
mat-select {
|
select {
|
||||||
--color: black;
|
|
||||||
color: var(--color);
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-option {
|
option {
|
||||||
--background: white;
|
--background: white;
|
||||||
|
background-color: var(--background);
|
||||||
color: var(--background);
|
color: var(--background);
|
||||||
font-family: (--ion-font-family);
|
font-family: var(--ion-font-family);
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@ -51,7 +47,8 @@ button {
|
|||||||
color: white;
|
color: white;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
width: 250px;
|
width: 250px;
|
||||||
margin-top: 100px;
|
margin-top: auto;
|
||||||
|
margin-bottom: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled,
|
button:disabled,
|
||||||
|
@ -15,7 +15,6 @@ export class EditAllowancePage implements OnInit {
|
|||||||
public goalId: number;
|
public goalId: number;
|
||||||
public userId: number;
|
public userId: number;
|
||||||
public isAddMode: boolean;
|
public isAddMode: boolean;
|
||||||
public selectedColor: string = '';
|
|
||||||
public possibleColors: Array<string> = [
|
public possibleColors: Array<string> = [
|
||||||
'#6199D9',
|
'#6199D9',
|
||||||
'#D98B61',
|
'#D98B61',
|
||||||
@ -74,7 +73,6 @@ export class EditAllowancePage implements OnInit {
|
|||||||
weight: allowance.weight,
|
weight: allowance.weight,
|
||||||
color: allowance.colour
|
color: allowance.colour
|
||||||
});
|
});
|
||||||
this.selectedColor = this.form.value.color;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ 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 { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@ -17,8 +16,7 @@ import { MatSelectModule } from '@angular/material/select';
|
|||||||
IonicModule,
|
IonicModule,
|
||||||
EditTaskPageRoutingModule,
|
EditTaskPageRoutingModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
MatIconModule,
|
MatIconModule
|
||||||
MatSelectModule
|
|
||||||
],
|
],
|
||||||
declarations: [EditTaskPage]
|
declarations: [EditTaskPage]
|
||||||
})
|
})
|
||||||
|
@ -6,6 +6,11 @@
|
|||||||
</div>
|
</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
|
||||||
|
*ngIf="!isAddMode"
|
||||||
|
class="remove-button"
|
||||||
|
(click)="deleteTask()"
|
||||||
|
>Delete task</button>
|
||||||
</div>
|
</div>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
@ -19,18 +24,13 @@
|
|||||||
<input id="reward" type="number" placeholder="0.00" name="price" min="0" value="0" step="0.01" formControlName="reward"/>
|
<input id="reward" 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>
|
||||||
|
@ -4,8 +4,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.remove-button {
|
.remove-button {
|
||||||
margin-top: 10px;
|
background-color: var(--ion-color-primary);
|
||||||
background-color: var(--negative-amount-color);
|
margin-right: 15px;
|
||||||
|
width: 95px;
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
@ -22,15 +24,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,7 +36,8 @@ button {
|
|||||||
color: white;
|
color: white;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
width: 250px;
|
width: 250px;
|
||||||
margin-top: 100px;
|
margin-top: auto;
|
||||||
|
margin-bottom: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled,
|
button:disabled,
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
.left {
|
.left {
|
||||||
width: 70%;
|
width: 70%;
|
||||||
font-size: 18px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.date {
|
.date {
|
||||||
|
@ -18,12 +18,8 @@
|
|||||||
<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">
|
<div class="assigned">{{ usernames[task.assigned ? task.assigned : 0] }}</div>
|
||||||
{{ 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 }"
|
||||||
|
@ -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,21 @@ 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 {
|
.assigned {
|
||||||
color: var(--line-color);
|
color: var(--line-color);
|
||||||
margin-left: 3px;
|
margin-left: 3px;
|
||||||
font-size: 15px;
|
font-size: 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 60%;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
}
|
@ -33,7 +33,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);
|
}, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
createTask() {
|
createTask() {
|
||||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 163 KiB |
@ -38,7 +38,6 @@
|
|||||||
|
|
||||||
ion-title {
|
ion-title {
|
||||||
color: var(--ion-color-primary);
|
color: var(--ion-color-primary);
|
||||||
font-size: 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ion-header {
|
ion-header {
|
||||||
@ -47,25 +46,4 @@ ion-header {
|
|||||||
|
|
||||||
button {
|
button {
|
||||||
font-size: 16px;
|
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);
|
|
||||||
}
|
}
|