12 Commits

Author SHA1 Message Date
ba1a67282a Re-add gronx
Some checks failed
Backend Build and Test / build (push) Failing after 2m27s
2025-10-01 20:57:45 +02:00
2aa58f1b65 Add API endpoint for transferring amounts between allowances
Some checks failed
Backend Build and Test / build (push) Failing after 5m57s
2025-10-01 20:50:14 +02:00
2dd06d4af3 Add transfer functionality between allowances 2025-10-01 20:50:13 +02:00
5a20e76df2 Improve compatibility with old browsers (#136)
All checks were successful
Backend Deploy / build (push) Successful in 3m16s
Backend Build and Test / build (push) Successful in 3m22s
Reviewed-on: #136
2025-05-29 13:54:52 +02:00
Huffle
02c5c6ea68 fix toolbar (#135)
All checks were successful
Backend Build and Test / build (push) Successful in 3m44s
Backend Deploy / build (push) Successful in 20s
closes #134

Reviewed-on: #135
2025-05-28 12:02:43 +02:00
Huffle
9cbb8756d1 fix select (#133)
All checks were successful
Backend Build and Test / build (push) Successful in 3m9s
Backend Deploy / build (push) Successful in 20s
closes #117

Reviewed-on: #133
2025-05-28 11:47:11 +02:00
Huffle
604b92b3b3 AP-116 (#131)
All checks were successful
Backend Build and Test / build (push) Successful in 3m17s
Backend Deploy / build (push) Successful in 18s
closes #130

Reviewed-on: #131
2025-05-28 10:27:11 +02:00
Huffle
c7236394d9 add app logo (#129)
Some checks failed
Backend Deploy / build (push) Successful in 17s
Backend Build and Test / build (push) Failing after 2m53s
closes #116

Reviewed-on: #129
2025-05-28 10:19:23 +02:00
Huffle
720ef83c2e change font size and move add and delete button in edit screens (#128)
All checks were successful
Backend Deploy / build (push) Successful in 18s
Backend Build and Test / build (push) Successful in 2m35s
closes #118
closes #119

Reviewed-on: #128
2025-05-28 10:00:18 +02:00
Huffle
5b1d107cac make done button bigger (#127)
All checks were successful
Backend Deploy / build (push) Successful in 20s
Backend Build and Test / build (push) Successful in 2m39s
closes #122

Reviewed-on: #127
2025-05-28 09:31:07 +02:00
Huffle
662257ebc5 fix statusbar (#126)
All checks were successful
Backend Deploy / build (push) Successful in 24s
Backend Build and Test / build (push) Successful in 2m59s
closes #123

Reviewed-on: #126
2025-05-28 09:18:29 +02:00
Huffle
ad48882bca icon (#115)
All checks were successful
Backend Build and Test / build (push) Successful in 3m16s
Backend Deploy / build (push) Successful in 22s
Reviewed-on: #115
2025-05-27 19:13:23 +02:00
57 changed files with 394 additions and 66 deletions

1
backend/.gitignore vendored
View File

@@ -1,3 +1,4 @@
*.db3
*.db3-*
*.db3.*
/allowance_planner

View File

@@ -2,10 +2,11 @@ package main
import (
"fmt"
"github.com/gavv/httpexpect/v2"
"strconv"
"testing"
"time"
"github.com/gavv/httpexpect/v2"
)
const (
@@ -914,3 +915,88 @@ func createTestAllowance(e *httpexpect.Expect, name string, target float64, weig
func createTestTask(e *httpexpect.Expect) int {
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().Contains("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

@@ -7,6 +7,8 @@ import (
"math"
"time"
"github.com/adhocore/gronx"
"gitea.seeseepuff.be/seeseemelk/mysqlite"
)
@@ -631,3 +633,74 @@ func (db *Db) AddAllowanceAmount(userId int, allowanceId int, request AddAllowan
return tx.Commit()
}
// TransferAllowance transfers amount from one allowance goal to another.
// Both allowance ids must exist and belong to the same user. The transfer
// will not move more than the 'to' goal still needs (target - balance).
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("allowances do not belong to the same user")
}
// 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("insufficient funds in source allowance")
}
// 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

@@ -78,3 +78,10 @@ type AddAllowanceAmountRequest struct {
Amount float64 `json:"amount"`
Description string `json:"description"`
}
// TransferRequest represents a request to transfer amount between two goals.
type TransferRequest struct {
From int `json:"from"`
To int `json:"to"`
Amount float64 `json:"amount"`
}

View File

@@ -4,9 +4,11 @@ go 1.24.2
require (
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/gin-contrib/cors v1.7.5
github.com/gin-gonic/gin v1.10.0
github.com/stretchr/testify v1.10.0
)
require (
@@ -47,7 +49,6 @@ require (
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sanity-io/litter v1.5.8 // 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/ugorji/go/codec v1.2.12 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect

View File

@@ -2,6 +2,8 @@ gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0 h1:aRItVfUj48fBmuec7rm/jY9KCfvHW
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/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/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=

View File

@@ -4,13 +4,16 @@ import (
"context"
"embed"
"errors"
"gitea.seeseepuff.be/seeseemelk/mysqlite"
"fmt"
"log"
"net"
"net/http"
"os"
"strconv"
"gitea.seeseepuff.be/seeseemelk/mysqlite"
"github.com/adhocore/gronx"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
@@ -43,6 +46,11 @@ type ServerConfig struct {
Started chan bool
}
const DefaultDomain = "localhost:8080"
// The domain that the server is reachable at.
var domain = DefaultDomain
func getUsers(c *gin.Context) {
users, err := db.GetUsers()
if err != nil {
@@ -431,6 +439,14 @@ func createTask(c *gin.Context) {
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 taskRequest.Assigned != nil {
exists, err := db.UserExists(*taskRequest.Assigned)
@@ -508,6 +524,11 @@ func putTask(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
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)
if err != nil {
@@ -706,5 +727,10 @@ func main() {
config.Datasource = "allowance_planner.db3"
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)
}

View File

@@ -3,6 +3,7 @@ package main
import (
"errors"
"github.com/gin-gonic/gin"
"log"
"net/http"
"strconv"
)
@@ -26,11 +27,22 @@ func loadWebEndpoints(router *gin.Engine) {
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) {
if c.Query("user") != "" {
c.SetCookie("user", c.Query("user"), 3600, "/", "localhost", false, true)
log.Println("Set cookie for user:", c.Query("user"))
c.SetCookie("user", c.Query("user"), 3600, "", "", false, true)
}
c.Redirect(http.StatusFound, "/")
redirectToPage(c, "/")
}
func renderIndex(c *gin.Context) {
@@ -68,7 +80,7 @@ func renderCreateTask(c *gin.Context) {
return
}
c.Redirect(http.StatusFound, "/")
redirectToPageStatus(c, "/", http.StatusFound)
}
func renderCompleteTask(c *gin.Context) {
@@ -85,7 +97,7 @@ func renderCompleteTask(c *gin.Context) {
return
}
c.Redirect(http.StatusFound, "/")
redirectToPageStatus(c, "/", http.StatusFound)
}
func renderCreateAllowance(c *gin.Context) {
@@ -122,7 +134,7 @@ func renderCreateAllowance(c *gin.Context) {
return
}
c.Redirect(http.StatusFound, "/")
redirectToPageStatus(c, "/", http.StatusFound)
}
func renderCompleteAllowance(c *gin.Context) {
@@ -144,11 +156,12 @@ func renderCompleteAllowance(c *gin.Context) {
return
}
c.Redirect(http.StatusFound, "/")
redirectToPageStatus(c, "/", http.StatusFound)
}
func getCurrentUser(c *gin.Context) *int {
currentUserStr, err := c.Cookie("user")
log.Println("Cookie string:", currentUserStr)
if errors.Is(err, http.ErrNoCookie) {
renderNoUser(c)
return nil
@@ -172,7 +185,7 @@ func getCurrentUser(c *gin.Context) *int {
func unsetUserCookie(c *gin.Context) {
c.SetCookie("user", "", -1, "/", "localhost", false, true)
c.Redirect(http.StatusFound, "/")
redirectToPageStatus(c, "/", http.StatusFound)
}
func renderNoUser(c *gin.Context) {

View File

@@ -3,9 +3,11 @@
<head>
<title>Allowance Planner 2000</title>
<style>
<!--
tr:hover {
background-color: #f0f0f0;
}
-->
</style>
</head>
<body>
@@ -27,7 +29,7 @@
{{if ne .CurrentUser 0}}
<h2>Allowances</h2>
<form action="/createAllowance" method="post">
<table border="1">
<table border=1>
<thead>
<tr>
<th>Name</th>
@@ -43,7 +45,7 @@
<td></td>
<td><label><input type="number" name="target" placeholder="Target"></label></td>
<td><label><input type="number" name="weight" placeholder="Weight"></label></td>
<td><button>Create</button></td>
<td><input type="submit" value="Create"></td>
</tr>
{{range .Allowances}}
{{if eq .ID 0}}
@@ -103,7 +105,7 @@
<td><label><input type="text" name="name" placeholder="Name"></label></td>
<td></td>
<td><label><input type="number" name="reward" placeholder="Reward"></label></td>
<td><button>Create</button></td>
<td><input type="submit" value="Create"></td>
</tr>
</tbody>
</table>

View File

@@ -409,6 +409,59 @@ paths:
404:
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:
schemas:
task:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

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

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View File

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

View File

@@ -18,6 +18,16 @@ form,
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;
@@ -30,8 +40,7 @@ button {
color: white;
padding: 10px;
width: 250px;
margin-top: auto;
margin-bottom: 50px;
margin-top: 100px;
}
button:disabled,

View File

@@ -36,7 +36,7 @@ export class AllowancePage implements ViewWillEnter {
allowance[0].name = 'Main Allowance';
this.allowance$.next(allowance);
})
}, 50);
}, 100);
}
canFinishGoal(allowance: Allowance): boolean {

View File

@@ -8,6 +8,7 @@ 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: [
@@ -16,7 +17,8 @@ import { MatIconModule } from '@angular/material/icon';
IonicModule,
EditAllowancePageRoutingModule,
ReactiveFormsModule,
MatIconModule
MatIconModule,
MatSelectModule
],
declarations: [EditAllowancePage]
})

View File

@@ -7,11 +7,6 @@
<ion-title *ngIf="isAddMode">Create Goal</ion-title>
<ion-title *ngIf="!isAddMode && goalId != 0">Edit Goal</ion-title>
<ion-title *ngIf="!isAddMode && goalId == 0">Edit Allowance</ion-title>
<button
*ngIf="!isAddMode && goalId !=0"
class="remove-button"
(click)="deleteAllowance()"
>Delete Goal</button>
</div>
</ion-toolbar>
</ion-header>
@@ -33,9 +28,9 @@
<div class="item" *ngIf="isAddMode || goalId != 0">
<label>Colour</label>
<select formControlName="color">
<option *ngFor="let color of possibleColors" [value]="color" [style.--background]="color">{{color}}</option>
</select>
<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()">
@@ -43,5 +38,10 @@
<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

@@ -4,10 +4,8 @@
}
.remove-button {
background-color: var(--ion-color-primary);
margin-right: 15px;
width: 100px;
margin-bottom: 0;
margin-top: 10px;
background-color: var(--negative-amount-color);
}
form {
@@ -28,17 +26,23 @@ label {
}
input,
select {
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);
}
option {
mat-option {
--background: white;
background-color: var(--background);
color: var(--background);
font-family: var(--ion-font-family);
font-family: (--ion-font-family);
}
button {
@@ -47,8 +51,7 @@ button {
color: white;
padding: 10px;
width: 250px;
margin-top: auto;
margin-bottom: 50px;
margin-top: 100px;
}
button:disabled,

View File

@@ -15,6 +15,7 @@ export class EditAllowancePage implements OnInit {
public goalId: number;
public userId: number;
public isAddMode: boolean;
public selectedColor: string = '';
public possibleColors: Array<string> = [
'#6199D9',
'#D98B61',
@@ -73,6 +74,7 @@ export class EditAllowancePage implements OnInit {
weight: allowance.weight,
color: allowance.colour
});
this.selectedColor = this.form.value.color;
}
});
}

View File

@@ -8,6 +8,7 @@ import { EditTaskPageRoutingModule } from './edit-task-routing.module';
import { EditTaskPage } from './edit-task.page';
import { MatIconModule } from '@angular/material/icon';
import { MatSelectModule } from '@angular/material/select';
@NgModule({
imports: [
@@ -16,7 +17,8 @@ import { MatIconModule } from '@angular/material/icon';
IonicModule,
EditTaskPageRoutingModule,
ReactiveFormsModule,
MatIconModule
MatIconModule,
MatSelectModule
],
declarations: [EditTaskPage]
})

View File

@@ -6,11 +6,6 @@
</div>
<ion-title *ngIf="isAddMode">Create Task</ion-title>
<ion-title *ngIf="!isAddMode">Edit Task</ion-title>
<button
*ngIf="!isAddMode"
class="remove-button"
(click)="deleteTask()"
>Delete task</button>
</div>
</ion-toolbar>
</ion-header>
@@ -24,13 +19,18 @@
<input id="reward" type="number" placeholder="0.00" name="price" min="0" value="0" step="0.01" formControlName="reward"/>
<label>Assigned</label>
<select formControlName="assigned">
<option *ngFor="let user of users" [value]="user.id">{{ user.name }}</option>
</select>
<mat-select formControlName="assigned">
<mat-option *ngFor="let user of users" [value]="user.id">{{ user.name }}</mat-option>
</mat-select>
<button type="button" [disabled]="!form.valid" (click)="submit()">
<span *ngIf="isAddMode">Add Task</span>
<span *ngIf="!isAddMode">Update Task</span>
</button>
<button
*ngIf="!isAddMode"
class="remove-button"
(click)="deleteTask()"
>Delete task</button>
</form>
</ion-content>

View File

@@ -4,10 +4,8 @@
}
.remove-button {
background-color: var(--ion-color-primary);
margin-right: 15px;
width: 95px;
margin-bottom: 0;
margin-top: 10px;
background-color: var(--negative-amount-color);
}
form {
@@ -24,10 +22,15 @@ label {
}
input,
select {
mat-select {
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);
}
button {
@@ -36,8 +39,7 @@ button {
color: white;
padding: 10px;
width: 250px;
margin-top: auto;
margin-bottom: 50px;
margin-top: 100px;
}
button:disabled,

View File

@@ -8,6 +8,7 @@
.left {
width: 70%;
font-size: 18px;
}
.date {

View File

@@ -18,8 +18,12 @@
<div class="task" *ngFor="let task of tasks$ | async">
<button (click)="completeTask(task.id)">Done</button>
<div (click)="updateTask(task.id)" class="item">
<div class="name">{{ task.name }}</div>
<div class="assigned">{{ usernames[task.assigned ? task.assigned : 0] }}</div>
<div class="text">
<div class="name">
{{ task.name }}
<span class="assigned">{{ usernames[task.assigned ? task.assigned : 0] }}</span>
</div>
</div>
<div
class="reward"
[ngClass]="{ 'negative': task.reward < 0 }"

View File

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

View File

@@ -33,7 +33,7 @@ export class TasksPage implements ViewWillEnter {
this.taskService.getTaskList().subscribe(tasks => {
this.tasks$.next(tasks);
});
}, 50);
}, 100);
}
createTask() {

View File

@@ -38,6 +38,7 @@
ion-title {
color: var(--ion-color-primary);
font-size: 24px;
}
ion-header {
@@ -46,4 +47,25 @@ ion-header {
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);
}