3 Commits

Author SHA1 Message Date
Huffle
46a4bfcd27 add app icon
All checks were successful
Backend Build and Test / build (push) Successful in 2m20s
2025-05-27 19:13:13 +02:00
Huffle
efc2453243 Merge branch 'main' of https://gitea.seeseepuff.be/seeseemelk/allowance_planner_2000 2025-05-27 19:08:37 +02:00
Huffle
2e81a635ee Change server url
All checks were successful
Backend Build and Test / build (push) Successful in 3m12s
2025-05-27 19:05:29 +02:00
57 changed files with 66 additions and 394 deletions

1
backend/.gitignore vendored
View File

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

View File

@@ -2,11 +2,10 @@ package main
import (
"fmt"
"github.com/gavv/httpexpect/v2"
"strconv"
"testing"
"time"
"github.com/gavv/httpexpect/v2"
)
const (
@@ -915,88 +914,3 @@ 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,8 +7,6 @@ import (
"math"
"time"
"github.com/adhocore/gronx"
"gitea.seeseepuff.be/seeseemelk/mysqlite"
)
@@ -633,74 +631,3 @@ 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,10 +78,3 @@ 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,11 +4,9 @@ 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 (
@@ -49,6 +47,7 @@ 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,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=
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,16 +4,13 @@ import (
"context"
"embed"
"errors"
"fmt"
"gitea.seeseepuff.be/seeseemelk/mysqlite"
"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"
)
@@ -46,11 +43,6 @@ 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 {
@@ -439,14 +431,6 @@ 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)
@@ -524,11 +508,6 @@ 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 {
@@ -727,10 +706,5 @@ 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,7 +3,6 @@ package main
import (
"errors"
"github.com/gin-gonic/gin"
"log"
"net/http"
"strconv"
)
@@ -27,22 +26,11 @@ 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") != "" {
log.Println("Set cookie for user:", c.Query("user"))
c.SetCookie("user", c.Query("user"), 3600, "", "", false, true)
c.SetCookie("user", c.Query("user"), 3600, "/", "localhost", false, true)
}
redirectToPage(c, "/")
c.Redirect(http.StatusFound, "/")
}
func renderIndex(c *gin.Context) {
@@ -80,7 +68,7 @@ func renderCreateTask(c *gin.Context) {
return
}
redirectToPageStatus(c, "/", http.StatusFound)
c.Redirect(http.StatusFound, "/")
}
func renderCompleteTask(c *gin.Context) {
@@ -97,7 +85,7 @@ func renderCompleteTask(c *gin.Context) {
return
}
redirectToPageStatus(c, "/", http.StatusFound)
c.Redirect(http.StatusFound, "/")
}
func renderCreateAllowance(c *gin.Context) {
@@ -134,7 +122,7 @@ func renderCreateAllowance(c *gin.Context) {
return
}
redirectToPageStatus(c, "/", http.StatusFound)
c.Redirect(http.StatusFound, "/")
}
func renderCompleteAllowance(c *gin.Context) {
@@ -156,12 +144,11 @@ func renderCompleteAllowance(c *gin.Context) {
return
}
redirectToPageStatus(c, "/", http.StatusFound)
c.Redirect(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
@@ -185,7 +172,7 @@ func getCurrentUser(c *gin.Context) *int {
func unsetUserCookie(c *gin.Context) {
c.SetCookie("user", "", -1, "/", "localhost", false, true)
redirectToPageStatus(c, "/", http.StatusFound)
c.Redirect(http.StatusFound, "/")
}
func renderNoUser(c *gin.Context) {

View File

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

View File

@@ -409,59 +409,6 @@ 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: 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"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<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>
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

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

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 = {
appId: 'io.ionic.starter',
appName: 'Allowance Planner V2',
appName: 'allowance-planner-v2',
webDir: 'www'
};

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,11 @@
<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>
@@ -28,9 +33,9 @@
<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>
<select formControlName="color">
<option *ngFor="let color of possibleColors" [value]="color" [style.--background]="color">{{color}}</option>
</select>
</div>
<button type="button" [disabled]="!form.valid" (click)="submit()">
@@ -38,10 +43,5 @@
<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,8 +4,10 @@
}
.remove-button {
margin-top: 10px;
background-color: var(--negative-amount-color);
background-color: var(--ion-color-primary);
margin-right: 15px;
width: 100px;
margin-bottom: 0;
}
form {
@@ -26,23 +28,17 @@ label {
}
input,
mat-select {
--color: black;
color: var(--color);
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);
}
mat-option {
option {
--background: white;
background-color: var(--background);
color: var(--background);
font-family: (--ion-font-family);
font-family: var(--ion-font-family);
}
button {
@@ -51,7 +47,8 @@ button {
color: white;
padding: 10px;
width: 250px;
margin-top: 100px;
margin-top: auto;
margin-bottom: 50px;
}
button:disabled,

View File

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

View File

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

View File

@@ -6,6 +6,11 @@
</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>
@@ -19,18 +24,13 @@
<input id="reward" type="number" placeholder="0.00" name="price" min="0" value="0" step="0.01" formControlName="reward"/>
<label>Assigned</label>
<mat-select formControlName="assigned">
<mat-option *ngFor="let user of users" [value]="user.id">{{ user.name }}</mat-option>
</mat-select>
<select formControlName="assigned">
<option *ngFor="let user of users" [value]="user.id">{{ user.name }}</option>
</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,8 +4,10 @@
}
.remove-button {
margin-top: 10px;
background-color: var(--negative-amount-color);
background-color: var(--ion-color-primary);
margin-right: 15px;
width: 95px;
margin-bottom: 0;
}
form {
@@ -22,15 +24,10 @@ label {
}
input,
mat-select {
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 {
@@ -39,7 +36,8 @@ button {
color: white;
padding: 10px;
width: 250px;
margin-top: 100px;
margin-top: auto;
margin-bottom: 50px;
}
button:disabled,

View File

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

View File

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

View File

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

View File

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

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

View File

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