10 Commits

Author SHA1 Message Date
Huffle
f04529067a update task functionality (#59)
Reviewed-on: #59
2025-05-18 16:43:53 +02:00
Huffle
6e07d44733 update task functionality 2025-05-18 16:34:49 +02:00
Huffle
1f21924805 AP-45 (#57)
Reviewed-on: #57
2025-05-18 16:18:07 +02:00
Huffle
e85a60ab16 Merge branch 'main' into AP-45 2025-05-18 16:17:13 +02:00
Huffle
61694e340f Add functionalty to add task 2025-05-18 16:15:34 +02:00
Huffle
f72cc8a802 test 2025-05-18 10:48:52 +02:00
da17f351de Add bulk allowance edit endpoint (#56)
Closes #15

Reviewed-on: #56
2025-05-18 09:24:36 +02:00
79dcfbc02c Implement completion endpoint for allowance (#55)
Closes #19

Reviewed-on: #55
2025-05-18 09:02:33 +02:00
505faa95a3 Add different cors config 2025-05-18 08:54:22 +02:00
Huffle
a675d51718 AP-45 wip post request 2025-05-18 08:52:20 +02:00
21 changed files with 611 additions and 62 deletions

View File

@@ -2,8 +2,15 @@
An improved Allowance Planner app. An improved Allowance Planner app.
## Running backend ## Running backend
In order to run the backend, go to the `backend directory and run: In order to run the backend, go to the `backend` directory and run:
```bash ```bash
$ go run . $ go run .
``` ```
## Running frontend
In order to run the frontend, go to the `allowance-planner-v2` directory in the `frontend` directory and run:
```bash
$ ionic serve
```

View File

@@ -361,10 +361,6 @@ func TestGetTaskWhenNoTasks(t *testing.T) {
result.Length().IsEqual(0) result.Length().IsEqual(0)
} }
func createTestTask(e *httpexpect.Expect) int {
return createTestTaskWithAmount(e, 100)
}
func createTestTaskWithAmount(e *httpexpect.Expect, amount int) int { func createTestTaskWithAmount(e *httpexpect.Expect, amount int) int {
requestBody := map[string]interface{}{ requestBody := map[string]interface{}{
"name": "Test Task", "name": "Test Task",
@@ -594,8 +590,152 @@ func TestCompleteTask(t *testing.T) {
} }
} }
func TestCompleteTaskAllowanceWeightsSumTo0(t *testing.T) {
e := startServer(t)
taskId := createTestTaskWithAmount(e, 101)
e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)
// Update rest allowance
e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{
Weight: 0,
}).Expect().Status(200)
// Create an allowance goal
createTestAllowance(e, "Test Allowance 1", 1000, 0)
// Complete the task
e.POST("/task/" + strconv.Itoa(taskId) + "/complete").Expect().Status(200)
// Verify the task is marked as completed
e.GET("/task/" + strconv.Itoa(taskId)).Expect().Status(404)
// Verify the allowances are updated for user 1
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Length().IsEqual(2)
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
allowances.Value(0).Object().Value("progress").Number().IsEqual(101)
allowances.Value(1).Object().Value("id").Number().IsEqual(1)
allowances.Value(1).Object().Value("progress").Number().IsEqual(0)
}
func TestCompleteTaskInvalidId(t *testing.T) {
e := startServer(t)
e.POST("/task/999/complete").Expect().Status(404)
}
func TestCompleteTaskAllowanceWeightsSumTo0(t *testing.T) {
e := startServer(t)
taskId := createTestTaskWithAmount(e, 101)
e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)
// Update rest allowance
e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{
Weight: 0,
}).Expect().Status(200)
// Create two allowance goals
e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
Name: "Test Allowance 1",
Target: 1000,
Weight: 0,
}).Expect().Status(201)
// Complete the task
e.POST("/task/" + strconv.Itoa(taskId) + "/complete").Expect().Status(200)
// Verify the task is marked as completed
e.GET("/task/" + strconv.Itoa(taskId)).Expect().Status(404)
// Verify the allowances are updated for user 1
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Length().IsEqual(2)
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
allowances.Value(0).Object().Value("progress").Number().IsEqual(101)
allowances.Value(1).Object().Value("id").Number().IsEqual(1)
allowances.Value(1).Object().Value("progress").Number().IsEqual(0)
}
func TestCompleteAllowance(t *testing.T) {
e := startServer(t)
createTestTaskWithAmount(e, 100)
createTestAllowance(e, "Test Allowance 1", 100, 50)
// Complete the task
e.POST("/task/1/complete").Expect().Status(200)
// Complete allowance goal
e.POST("/user/1/allowance/1/complete").Expect().Status(200)
// Verify the allowance no longer exists
e.GET("/user/1/allowance/1").Expect().Status(404)
// Verify history is updated
history := e.GET("/user/1/history").Expect().Status(200).JSON().Array()
history.Length().IsEqual(2)
history.Value(0).Object().Value("allowance").Number().IsEqual(100)
history.Value(0).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
history.Value(1).Object().Value("allowance").Number().IsEqual(-100)
history.Value(1).Object().Value("timestamp").String().AsDateTime().InRange(getDelta(time.Now(), 2.0))
}
func TestCompleteAllowanceInvalidUserId(t *testing.T) {
e := startServer(t)
e.POST("/user/999/allowance/1/complete").Expect().Status(404)
}
func TestCompleteAllowanceInvalidAllowanceId(t *testing.T) {
e := startServer(t)
e.POST("/user/1/allowance/999/complete").Expect().Status(404)
}
func TestPutBulkAllowance(t *testing.T) {
e := startServer(t)
createTestAllowance(e, "Test Allowance 1", 1000, 1)
createTestAllowance(e, "Test Allowance 2", 1000, 2)
// Bulk edit
request := []map[string]interface{}{
{
"id": 1,
"weight": 5,
},
{
"id": 0,
"weight": 99,
},
{
"id": 2,
"weight": 10,
},
}
e.PUT("/user/1/allowance").WithJSON(request).Expect().Status(200)
// Verify the allowances are updated
allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
allowances.Length().IsEqual(3)
allowances.Value(0).Object().Value("id").Number().IsEqual(0)
allowances.Value(0).Object().Value("weight").Number().IsEqual(99)
allowances.Value(1).Object().Value("id").Number().IsEqual(1)
allowances.Value(1).Object().Value("weight").Number().IsEqual(5)
allowances.Value(2).Object().Value("id").Number().IsEqual(2)
allowances.Value(2).Object().Value("weight").Number().IsEqual(10)
}
func getDelta(base time.Time, delta float64) (time.Time, time.Time) { func getDelta(base time.Time, delta float64) (time.Time, time.Time) {
start := base.Add(-time.Duration(delta) * time.Second) start := base.Add(-time.Duration(delta) * time.Second)
end := base.Add(time.Duration(delta) * time.Second) end := base.Add(time.Duration(delta) * time.Second)
return start, end return start, end
} }
func createTestAllowance(e *httpexpect.Expect, name string, target int, weight float64) {
e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
Name: name,
Target: target,
Weight: weight,
}).Expect().Status(201)
}
func createTestTask(e *httpexpect.Expect) int {
return createTestTaskWithAmount(e, 100)
}

View File

@@ -175,6 +175,39 @@ func (db *Db) DeleteAllowance(userId int, allowanceId int) error {
return nil return nil
} }
func (db *Db) CompleteAllowance(userId int, allowanceId int) error {
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
// Get the cost of the allowance
var cost int
err = tx.Query("select balance from allowances where id = ? and user_id = ?").
Bind(allowanceId, userId).ScanSingle(&cost)
if err != nil {
return err
}
// Delete the allowance
err = tx.Query("delete from allowances where id = ? and user_id = ?").
Bind(allowanceId, userId).Exec()
if err != nil {
return err
}
// Add a history entry
err = tx.Query("insert into history (user_id, timestamp, amount) values (?, ?, ?)").
Bind(userId, time.Now().Unix(), -cost).
Exec()
if err != nil {
return err
}
return tx.Commit()
}
func (db *Db) UpdateUserAllowance(userId int, allowance *UpdateAllowanceRequest) error { func (db *Db) UpdateUserAllowance(userId int, allowance *UpdateAllowanceRequest) error {
tx, err := db.db.Begin() tx, err := db.db.Begin()
if err != nil { if err != nil {
@@ -218,6 +251,31 @@ func (db *Db) UpdateAllowance(userId int, allowanceId int, allowance *UpdateAllo
return tx.Commit() return tx.Commit()
} }
func (db *Db) BulkUpdateAllowance(userId int, allowances []BulkUpdateAllowanceRequest) error {
tx, err := db.db.Begin()
if err != nil {
return err
}
defer tx.MustRollback()
for _, allowance := range allowances {
if allowance.ID == 0 {
err = tx.Query("update users set weight=? where id = ?").
Bind(allowance.Weight, userId).
Exec()
} else {
err = tx.Query("update allowances set weight=? where id = ? and user_id = ?").
Bind(allowance.Weight, allowance.ID, userId).
Exec()
}
if err != nil {
return err
}
}
return tx.Commit()
}
func (db *Db) CreateTask(task *CreateTaskRequest) (int, error) { func (db *Db) CreateTask(task *CreateTaskRequest) (int, error) {
tx, err := db.db.Begin() tx, err := db.db.Begin()
if err != nil { if err != nil {
@@ -356,6 +414,7 @@ func (db *Db) CompleteTask(taskId int) error {
remainingReward := reward remainingReward := reward
if sumOfWeights > 0 {
// Distribute the reward to the allowances // Distribute the reward to the allowances
for allowanceRow := range tx.Query("select id, weight from allowances where user_id = ? and weight > 0").Bind(userId).Range(&err) { for allowanceRow := range tx.Query("select id, weight from allowances where user_id = ? and weight > 0").Bind(userId).Range(&err) {
var allowanceId int var allowanceId int
@@ -375,6 +434,7 @@ func (db *Db) CompleteTask(taskId int) error {
} }
remainingReward -= amount remainingReward -= amount
} }
}
// Add the remaining reward to the user // Add the remaining reward to the user
err = tx.Query("update users set balance = balance + ? where id = ?"). err = tx.Query("update users set balance = balance + ? where id = ?").

View File

@@ -35,19 +35,24 @@ type Allowance struct {
Name string `json:"name"` Name string `json:"name"`
Target int `json:"target"` Target int `json:"target"`
Progress int `json:"progress"` Progress int `json:"progress"`
Weight int `json:"weight"` Weight float64 `json:"weight"`
} }
type CreateAllowanceRequest struct { type CreateAllowanceRequest struct {
Name string `json:"name"` Name string `json:"name"`
Target int `json:"target"` Target int `json:"target"`
Weight int `json:"weight"` Weight float64 `json:"weight"`
} }
type UpdateAllowanceRequest struct { type UpdateAllowanceRequest struct {
Name string `json:"name"` Name string `json:"name"`
Target int `json:"target"` Target int `json:"target"`
Weight int `json:"weight"` Weight float64 `json:"weight"`
}
type BulkUpdateAllowanceRequest struct {
ID int `json:"id"`
Weight float64 `json:"weight"`
} }
type CreateGoalResponse struct { type CreateGoalResponse struct {

View File

@@ -189,6 +189,44 @@ func createUserAllowance(c *gin.Context) {
c.IndentedJSON(http.StatusCreated, response) c.IndentedJSON(http.StatusCreated, response)
} }
func bulkPutUserAllowance(c *gin.Context) {
userIdStr := c.Param("userId")
userId, err := strconv.Atoi(userIdStr)
if err != nil {
log.Printf(ErrInvalidUserID+": %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID})
return
}
exists, err := db.UserExists(userId)
if err != nil {
log.Printf(ErrCheckingUserExist, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
if !exists {
c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound})
return
}
var allowanceRequest []BulkUpdateAllowanceRequest
if err := c.ShouldBindJSON(&allowanceRequest); err != nil {
log.Printf("Error parsing request body: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
err = db.BulkUpdateAllowance(userId, allowanceRequest)
if err != nil {
log.Printf("Error updating allowance: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
c.IndentedJSON(http.StatusOK, gin.H{"message": "Allowance updated successfully"})
}
func deleteUserAllowance(c *gin.Context) { func deleteUserAllowance(c *gin.Context) {
userIdStr := c.Param("userId") userIdStr := c.Param("userId")
allowanceIdStr := c.Param("allowanceId") allowanceIdStr := c.Param("allowanceId")
@@ -287,6 +325,49 @@ func putUserAllowance(c *gin.Context) {
c.IndentedJSON(http.StatusOK, gin.H{"message": "Allowance updated successfully"}) c.IndentedJSON(http.StatusOK, gin.H{"message": "Allowance updated successfully"})
} }
func completeAllowance(c *gin.Context) {
userIdStr := c.Param("userId")
allowanceIdStr := c.Param("allowanceId")
userId, err := strconv.Atoi(userIdStr)
if err != nil {
log.Printf(ErrInvalidUserID+": %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidUserID})
return
}
allowanceId, err := strconv.Atoi(allowanceIdStr)
if err != nil {
log.Printf("Invalid allowance ID: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid allowance ID"})
return
}
exists, err := db.UserExists(userId)
if err != nil {
log.Printf(ErrCheckingUserExist, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
if !exists {
c.JSON(http.StatusNotFound, gin.H{"error": ErrUserNotFound})
return
}
err = db.CompleteAllowance(userId, allowanceId)
if errors.Is(err, mysqlite.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "Allowance not found"})
return
}
if err != nil {
log.Printf("Error completing allowance: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
return
}
c.IndentedJSON(http.StatusOK, gin.H{"message": "Allowance completed successfully"})
}
func createTask(c *gin.Context) { func createTask(c *gin.Context) {
var taskRequest CreateTaskRequest var taskRequest CreateTaskRequest
if err := c.ShouldBindJSON(&taskRequest); err != nil { if err := c.ShouldBindJSON(&taskRequest); err != nil {
@@ -506,18 +587,22 @@ func start(ctx context.Context, config *ServerConfig) {
defer db.db.MustClose() defer db.db.MustClose()
router := gin.Default() router := gin.Default()
router.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"}, corsConfig := cors.DefaultConfig()
})) corsConfig.AllowAllOrigins = true
router.Use(cors.New(corsConfig))
router.GET("/api/users", getUsers) router.GET("/api/users", getUsers)
router.GET("/api/user/:userId", getUser) router.GET("/api/user/:userId", getUser)
router.POST("/api/user/:userId/history", postHistory) router.POST("/api/user/:userId/history", postHistory)
router.GET("/api/user/:userId/history", getHistory) router.GET("/api/user/:userId/history", getHistory)
router.GET("/api/user/:userId/allowance", getUserAllowance) router.GET("/api/user/:userId/allowance", getUserAllowance)
router.POST("/api/user/:userId/allowance", createUserAllowance) router.POST("/api/user/:userId/allowance", createUserAllowance)
router.PUT("/api/user/:userId/allowance", bulkPutUserAllowance)
router.GET("/api/user/:userId/allowance/:allowanceId", getUserAllowanceById) router.GET("/api/user/:userId/allowance/:allowanceId", getUserAllowanceById)
router.DELETE("/api/user/:userId/allowance/:allowanceId", deleteUserAllowance) router.DELETE("/api/user/:userId/allowance/:allowanceId", deleteUserAllowance)
router.PUT("/api/user/:userId/allowance/:allowanceId", putUserAllowance) router.PUT("/api/user/:userId/allowance/:allowanceId", putUserAllowance)
router.POST("/api/user/:userId/allowance/:allowanceId/complete", completeAllowance)
router.POST("/api/tasks", createTask) router.POST("/api/tasks", createTask)
router.GET("/api/tasks", getTasks) router.GET("/api/tasks", getTasks)
router.GET("/api/task/:taskId", getTask) router.GET("/api/task/:taskId", getTask)
@@ -558,5 +643,9 @@ func main() {
Datasource: os.Getenv("DB_PATH"), Datasource: os.Getenv("DB_PATH"),
Addr: ":8080", Addr: ":8080",
} }
if config.Datasource == "" {
config.Datasource = "allowance_planner.db3"
log.Printf("Warning: No DB_PATH set, using default of %s", config.Datasource)
}
start(context.Background(), &config) start(context.Background(), &config)
} }

View File

@@ -2,7 +2,7 @@ create table users
( (
id integer primary key, id integer primary key,
name text not null, name text not null,
weight real not null default 1.0, weight real not null default 0.0,
balance integer not null default 0 balance integer not null default 0
) strict; ) strict;

View File

@@ -11,6 +11,7 @@ const routes: Routes = [
path: '', path: '',
loadChildren: () => import('./pages/tabs/tabs.module').then(m => m.TabsPageModule) loadChildren: () => import('./pages/tabs/tabs.module').then(m => m.TabsPageModule)
}, },
]; ];
@NgModule({ @NgModule({
imports: [ imports: [

View File

@@ -3,11 +3,12 @@ import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router'; import { RouteReuseStrategy } from '@angular/router';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { Drivers, Storage } from '@ionic/storage'; import { Drivers } from '@ionic/storage';
import { IonicStorageModule } from '@ionic/storage-angular'; import { IonicStorageModule } from '@ionic/storage-angular';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({ @NgModule({
declarations: [AppComponent], declarations: [AppComponent],
@@ -15,6 +16,7 @@ import { AppComponent } from './app.component';
BrowserModule, BrowserModule,
IonicModule.forRoot(), IonicModule.forRoot(),
AppRoutingModule, AppRoutingModule,
ReactiveFormsModule,
IonicStorageModule.forRoot({ IonicStorageModule.forRoot({
name: '__mydb', name: '__mydb',
driverOrder: [Drivers.IndexedDB, Drivers.LocalStorage] driverOrder: [Drivers.IndexedDB, Drivers.LocalStorage]

View File

@@ -2,5 +2,5 @@ export interface Task {
id: number; id: number;
name: string; name: string;
reward: number; reward: number;
assigned: number; assigned: number | null;
} }

View File

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

View File

@@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { EditTaskPageRoutingModule } from './edit-task-routing.module';
import { EditTaskPage } from './edit-task.page';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
EditTaskPageRoutingModule,
ReactiveFormsModule
],
declarations: [EditTaskPage]
})
export class EditTaskPageModule {}

View File

@@ -0,0 +1,26 @@
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title *ngIf="isAddMode">Create Task</ion-title>
<ion-title *ngIf="!isAddMode">Edit Task</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<form [formGroup]="form">
<label>Task Name</label>
<input id="name" type="text" formControlName="name"/>
<label>Reward</label>
<input id="name" type="number" formControlName="reward"/>
<label>Assigned</label>
<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>
</form>
</ion-content>

View File

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

View File

@@ -0,0 +1,17 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EditTaskPage } from './edit-task.page';
describe('EditTaskPage', () => {
let component: EditTaskPage;
let fixture: ComponentFixture<EditTaskPage>;
beforeEach(() => {
fixture = TestBed.createComponent(EditTaskPage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,75 @@
import { Location } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { User } from 'src/app/models/user';
import { TaskService } from 'src/app/services/task.service';
import { UserService } from 'src/app/services/user.service';
@Component({
selector: 'app-edit-task',
templateUrl: './edit-task.page.html',
styleUrls: ['./edit-task.page.scss'],
standalone: false,
})
export class EditTaskPage implements OnInit {
form: FormGroup;
id: number;
isAddMode: boolean;
users: Array<User> = [{id: 0, name: 'unassigned'}];
constructor(
private route: ActivatedRoute,
private formBuilder: FormBuilder,
private taskService: TaskService,
private userService: UserService,
private router: Router
) {
this.id = this.route.snapshot.params['id'];
this.isAddMode = !this.id;
this.form = this.formBuilder.group({
name: ['', Validators.required],
reward: ['', [Validators.required, Validators.pattern("^[0-9]*$")]],
assigned: [0, Validators.required]
});
}
ngOnInit() {
this.userService.getUserList().subscribe(users => {
this.users.push(...users);
});
if (!this.isAddMode) {
this.taskService.getTaskById(this.id).subscribe(task => {
this.form.setValue({
name: task.name,
reward: task.reward,
assigned: task.assigned !== null ? task.assigned : 0
});
});
}
}
submit() {
const formValue = this.form.value;
let assigned: number | null = Number(formValue.assigned);
if (assigned === 0) {
assigned = null;
}
const task = {
name: formValue.name,
reward: formValue.reward,
assigned
}
if (this.isAddMode) {
this.taskService.createTask(task);
} else {
this.taskService.updateTask(this.id, task);
}
this.router.navigate(['/tabs/tasks']);
}
}

View File

@@ -17,7 +17,7 @@ const routes: Routes = [
}, },
{ {
path: 'tasks', path: 'tasks',
loadChildren: () => import('../tasks/tasks.module').then(m => m.TasksPageModule) loadChildren: () => import('../tasks/tasks.module').then(m => m.TasksPageModule),
}, },
{ {
path: '', path: '',

View File

@@ -6,7 +6,9 @@ const routes: Routes = [
{ {
path: '', path: '',
component: TasksPage, component: TasksPage,
} },
{ path: 'add', loadChildren: () => import('../edit-task/edit-task.module').then(m => m.EditTaskPageModule) },
{ path: 'edit/:id', loadChildren: () => import('../edit-task/edit-task.module').then(m => m.EditTaskPageModule) }
]; ];
@NgModule({ @NgModule({

View File

@@ -1,17 +1,21 @@
<ion-header [translucent]="true" class="ion-no-border"> <ion-header [translucent]="true" class="ion-no-border">
<ion-toolbar> <ion-toolbar>
<div class="toolbar">
<ion-title> <ion-title>
Tasks Tasks
</ion-title> </ion-title>
<button class="add-button" (click)="createTask()">Add task</button>
</div>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content> <ion-content>
<div class="content">
<div class="icon"> <div class="icon">
<mat-icon>filter_alt</mat-icon> <mat-icon>filter_alt</mat-icon>
</div> </div>
<div class="list"> <div class="list">
<div class="task" *ngFor="let task of tasks"> <div class="task" (click)="updateTask(task.id)" *ngFor="let task of tasks$ | async">
<button>Done</button> <button>Done</button>
<div class="name">{{ task.name }}</div> <div class="name">{{ task.name }}</div>
<div <div
@@ -20,4 +24,5 @@
>{{ task.reward.toFixed(2) }} SP</div> >{{ task.reward.toFixed(2) }} SP</div>
</div> </div>
</div> </div>
</div>
</ion-content> </ion-content>

View File

@@ -1,3 +1,13 @@
.toolbar {
display: flex;
}
.content {
display: flex;
flex-direction: column;
height: 100%;
}
.icon { .icon {
padding: 5px; padding: 5px;
display: flex; display: flex;
@@ -45,3 +55,9 @@ button {
color: white; color: white;
background: var(--confirm-button-color); background: var(--confirm-button-color);
} }
.add-button {
background-color: var(--ion-color-primary);
margin-right: 15px;
width: 75px;
}

View File

@@ -1,24 +1,43 @@
import { Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { TaskService } from 'src/app/services/task.service'; import { TaskService } from 'src/app/services/task.service';
import { Task } from 'src/app/models/task'; import { Task } from 'src/app/models/task';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject } from 'rxjs';
import { ViewWillEnter } from '@ionic/angular';
@Component({ @Component({
selector: 'app-tasks', selector: 'app-tasks',
templateUrl: 'tasks.page.html', templateUrl: 'tasks.page.html',
styleUrls: ['tasks.page.scss'], styleUrls: ['tasks.page.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false, standalone: false,
}) })
export class TasksPage implements OnInit { export class TasksPage implements ViewWillEnter {
public tasks: Array<Task> = []; public tasks$: BehaviorSubject<Array<Task>> = new BehaviorSubject<Array<Task>>([]);
constructor( constructor(
private taskService: TaskService private taskService: TaskService,
) {} private router: Router,
private route: ActivatedRoute
) {
this.getTasks();
}
ngOnInit(): void { ionViewWillEnter(): void {
this.getTasks();
}
getTasks() {
this.taskService.getTaskList().subscribe(tasks => { this.taskService.getTaskList().subscribe(tasks => {
this.tasks = tasks; this.tasks$.next(tasks);
}); });
} }
createTask() {
this.router.navigate(['add'], { relativeTo: this.route });
}
updateTask(id: number) {
this.router.navigate(['edit', id], { relativeTo: this.route });
}
} }

View File

@@ -7,10 +7,23 @@ import { Task } from '../models/task';
providedIn: 'root' providedIn: 'root'
}) })
export class TaskService { export class TaskService {
private url = 'http://localhost:8080/api' private url = 'http://localhost:8080/api';
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
getTaskList(): Observable<Array<Task>> { getTaskList(): Observable<Array<Task>> {
return this.http.get<Task[]>(`${this.url}/tasks`); return this.http.get<Task[]>(`${this.url}/tasks`);
} }
getTaskById(taskId: number): Observable<Task> {
return this.http.get<Task>(`${this.url}/task/${taskId}`);
}
createTask(task: Partial<Task>) {
this.http.post(`${this.url}/tasks`, task).subscribe();
}
updateTask(id: number, task: Partial<Task>) {
this.http.put(`${this.url}/task/${id}`, task).subscribe();
}
} }