AP-63 (#85)
All checks were successful
Backend Deploy / build (push) Successful in 29s
Backend Build and Test / build (push) Successful in 3m3s

closes #63

Reviewed-on: #85
This commit is contained in:
2025-05-25 17:05:31 +02:00
parent 302ceaa629
commit daebcdeccd
170 changed files with 10517 additions and 9 deletions

View File

@@ -0,0 +1,10 @@
export interface Allowance {
id: number;
name: string;
target: number;
// Current allowance value
progress: number;
// Can be any positive number (backend checks for number relative to each other)
weight: number;
colour: string;
}

View File

@@ -5,14 +5,22 @@ import { FormsModule } from '@angular/forms';
import { AllowancePage } from './allowance.page';
import { AllowancePageRoutingModule } from './allowance-routing.module';
import { AllowanceService } from 'src/app/services/allowance.service';
import { provideHttpClient } from '@angular/common/http';
import { MatIconModule } from '@angular/material/icon';
@NgModule({
imports: [
IonicModule,
CommonModule,
FormsModule,
AllowancePageRoutingModule
AllowancePageRoutingModule,
MatIconModule
],
declarations: [AllowancePage]
declarations: [AllowancePage],
providers: [
provideHttpClient(),
AllowanceService
]
})
export class AllowancePageModule {}

View File

@@ -7,4 +7,63 @@
</ion-header>
<ion-content>
<div class="content" *ngIf="allowance$ | async as allowance">
<div class="bar">
<div class="distribution">Allowance distribution</div>
<div class="allowance-bar">
<span
*ngFor="let goal of allowance"
class="partition"
[style.--partition-color]="goal.colour"
[style.width.%]="getPartitionSize(goal, allowance)"
></span>
</div>
<div class="legend">
<div class="legend-item" [style.--legend-color]="goal.colour" *ngFor="let goal of allowance">
<div class="circle"></div>
<div class="title">{{ goal.name }}</div>
</div>
</div>
</div>
<div
class="goal"
[style.--used-color]="goal.colour"
[ngClass]="{'other-goals': goal.id !== 0}"
*ngFor="let goal of allowance"
>
<div class="main" *ngIf="goal.id === 0; else other_goal">
<div class="title">
<div class="name">Main Allowance</div>
<div class="icon">
<mat-icon>settings</mat-icon>
</div>
</div>
<div class="progress">{{ goal.progress }} SP</div>
<div class="buttons">
<button class="add-button">Add</button>
<!-- <button class="move-button">Move</button> -->
<button class="spend-button">Spend</button>
</div>
</div>
<ng-template #other_goal>
<div class="color-wrapper">
<div>
<div class="title">
<div class="name">{{ goal.name }}</div>
<div class="icon">
<mat-icon>settings</mat-icon>
</div>
</div>
<div class="progress">{{ goal.progress }} / {{ goal.target }} SP</div>
<div class="buttons">
<button class="add-button">Add</button>
<!-- <button class="move-button">Move</button> -->
<button class="spend-button" [disabled]="!canFinishGoal(goal)">Finish goal</button>
</div>
</div>
<div class="color" [style.--background]="hexToRgb(goal.colour)" [style.width.%]="getPercentage(goal)"></div>
</div>
</ng-template>
</div>
</div>
</ion-content>

View File

@@ -0,0 +1,130 @@
.goal {
border: 1px solid var(--used-color);
border-radius: 10px;
padding: 10px;
margin-bottom: 20px;
margin-left: 10px;
margin-right: 10px;
color: var(--used-color);
}
.name {
font-size: 20px;
}
.progress {
color: var(--font-color);
margin-left: 15px;
margin-top: 8px;
margin-bottom: 15px;
font-size: 16px;
}
.bar {
margin-top: 20px;
margin-bottom: 20px;
margin-left: 20px;
}
.distribution {
color: var(--ion-color-primary);
}
.allowance-bar {
display: flex;
width: 95%;
height: 15px !important;
border-radius: 15px;
background-color: var(--font-color);
overflow: hidden;
}
.partition {
--partition-color: white;
background-color: var(--partition-color);
width: 25%;
height: 100%;
//border-radius: 15px;
}
.buttons,
.title {
display: flex;
gap: 10px;
}
button {
height: 30px;
padding-inline: 30px;
border-radius: 10px;
color: white;
font-size: 16px;
}
button:disabled,
button[disabled]{
opacity: 0.5;
}
.add-button {
background-color: var(--confirm-button-color);
}
.move-button {
background-color: var(--ion-color-primary);
}
.spend-button {
background-color: var(--negative-amount-color);
}
.icon {
margin-left: auto;
color: var(--font-color);
}
.color-wrapper {
padding: 10px;
border-radius: 9px;
position: relative;
z-index: 1;
}
.color {
--background: rgba(0, 0, 0, 0.3);
background-color: var(--background);
border-radius: 9px;
position: absolute;
top: 0;
bottom: 0;
left: 0;
z-index: -1;
}
.other-goals {
padding: unset;
}
.legend {
width: 95%;
display: flex;
font-size: 13px;
gap: 8px;
margin-top: 5px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
--legend-color: white;
color: var(--legend-color);
align-items: center;
}
.circle {
width: 12px;
height: 12px;
background-color: var(--legend-color);
border-radius: 20px;
margin-right: 2px;
}

View File

@@ -1,5 +1,10 @@
import { Component } from '@angular/core';
import { UserService } from 'src/app/services/user.service';
import { ActivatedRoute } from '@angular/router';
import { BehaviorSubject } from 'rxjs';
import { Allowance } from 'src/app/models/allowance';
import { AllowanceService } from 'src/app/services/allowance.service';
import hexRgb from 'hex-rgb';
import { ViewWillEnter } from '@ionic/angular';
@Component({
selector: 'app-allowance',
@@ -7,8 +12,64 @@ import { UserService } from 'src/app/services/user.service';
styleUrls: ['allowance.page.scss'],
standalone: false,
})
export class AllowancePage {
export class AllowancePage implements ViewWillEnter {
private id: number;
// Move to add/edit page later
private possibleColors: Array<string> = [
'#6199D9',
'#D98B61',
'#DBC307',
'#13DEB5',
'#7DCB7D',
'#CF1DBD',
'#F53311',
'#2F00FF',
'#098B0D',
'#1BC2E8'
];
public allowance$: BehaviorSubject<Array<Allowance>> = new BehaviorSubject<Array<Allowance>>([]);
constructor(private userService: UserService) {}
constructor(
private route: ActivatedRoute,
private allowanceService: AllowanceService
) {
this.id = this.route.snapshot.params['id'];
this.getAllowance();
}
ionViewWillEnter(): void {
this.getAllowance();
}
getAllowance() {
setTimeout(() => {
this.allowanceService.getAllowanceList(this.id).subscribe(allowance => {
allowance[0].colour = '#9C4BE4';
allowance[0].name = 'Main Allowance';
console.log('Allowance list: ', allowance);
this.allowance$.next(allowance);
})
}, 10);
}
canFinishGoal(allowance: Allowance): boolean {
return allowance.progress >= allowance.target;
}
hexToRgb(color: string) {
return hexRgb(color, { alpha: 0.3, format: 'css' })
}
getPercentage(allowance: Allowance): number {
return allowance.progress / allowance.target * 100;
}
// Returns number in percent
getPartitionSize(goal: Allowance, allowanceList: Array<Allowance>): number {
let allowanceTotal = 0;
for (let allowance of allowanceList) {
allowanceTotal += allowance.progress;
}
return goal.progress / allowanceTotal * 100;
}
}

View File

@@ -18,7 +18,7 @@
<input id="name" type="text" formControlName="name"/>
<label>Reward</label>
<input id="name" type="number" formControlName="reward"/>
<input id="name" type="number" placeholder="0.00" name="price" min="0" value="0" step="0.01" formControlName="reward"/>
<label>Assigned</label>
<select formControlName="assigned">

View File

@@ -30,7 +30,7 @@ export class EditTaskPage implements OnInit {
this.form = this.formBuilder.group({
name: ['', Validators.required],
reward: ['', [Validators.required, Validators.pattern("^[0-9]*$")]],
reward: ['', Validators.required],
assigned: [0, Validators.required]
});
}

View File

@@ -0,0 +1,17 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Allowance } from '../models/allowance';
@Injectable({
providedIn: 'root'
})
export class AllowanceService {
private url = 'http://localhost:8080/api';
constructor(private http: HttpClient) {}
getAllowanceList(userId: number): Observable<Array<Allowance>> {
return this.http.get<Allowance[]>(`${this.url}/user/${userId}/allowance`);
}
}