New version of asset calculator on signals

This commit is contained in:
Artur 2024-11-21 19:14:22 +01:00
parent 8893499650
commit 8f86c6ed53
5 changed files with 268 additions and 211 deletions

View File

@ -10,7 +10,7 @@ import { FriendlyPagesComponent } from './friendly-pages/friendly-pages.componen
export const routes: Routes = [
{
path: "",
title: "Sygnały",
title: "Kalkulator amortyzacyjny",
component:AssetCalculatorComponent
},
{

View File

@ -4,37 +4,39 @@
<div class="row">
<div class="h1 text-center text-lg-start" i18n="@@depreciationCalculator">
<div class="h1 text-center text-lg-start">
{{ 'asset-calculator.depreciationCalculator' | translate }}
</div>
</div>
<div class="row">
<div class="col mx-2 mx-lg-0 mb-2 col-lg-4 frame">
<div class="row text-center h5 "><h5 i18n="@@fixedAsset">{{ 'asset-calculator.fixedAsset' | translate }}</h5></div>
<form [formGroup]=assetsDepreciationFormGroup >
<div class="row text-center h5 "><h5>{{ 'asset-calculator.fixedAsset' | translate }}</h5></div>
<div class="form-group row align-items-center mb-1">
<div class="col-5 col-lg-6 text-start">
<label class="form-label" for="initialValueAsset" i18n="@@initialValue" >
<label class="form-label" for="initialValueAsset" >
{{ 'asset-calculator.initialValue' | translate }}</label>
</div>
<div class="col col-lg">
<input class="form-control" formControlName="initialValueAsset" type="number" id="initialValueAsset" required >
<input class="form-control" [(ngModel)]="initialValueAsset"
type="number"
id="initialValueAsset"
required >
@if(assetsDepreciationFormGroup.get('initialValueSet')?.invalid && assetsDepreciationFormGroup.get('initialValueSet')?.touched){
<!-- @if(assetsDepreciationFormGroup.get('initialValueSet')?.invalid && assetsDepreciationFormGroup.get('initialValueSet')?.touched){
<div class="text-danger col-auto"> Wartość niepoprawna!. Podaj kwotę w zł np 3000.05 (czyli 3000 zł i 5 gr)</div>
}@else{
}
-->
</div>
</div>
<div class="form-group row align-items-center">
<div class="col-5 col-lg-6 text-start ">
<label class="form-label" for="rate" >{{ 'asset-calculator.depreciationRate' | translate }}</label>
<label class="form-label" for="rate" >{{ 'asset-calculator.depreciationRate' | translate }}</label>
</div>
<div class="col-3 col-lg-3">
<input class="form-control" formControlName="rate" type="number" id="rate" />
<input class="form-control" [(ngModel)]="rate" type="number" id="rate" />
</div>
<div class="col-1 col-lg-1 text-start">
<label for="rate">%</label>
@ -46,7 +48,7 @@
<label class="form-label" for="startDepreciation" >{{ 'asset-calculator.startOfDepreciation' | translate }} </label>
</div>
<div class="col-7 col-lg-6">
<input type="month" lang="pl" class="form-control" formControlName="startDepreciation" id="startDepreciation" placeholder="2024" />
<input type="month" lang="pl" class="form-control" [(ngModel)]="startDepreciation" id="startDepreciation" placeholder="2024" />
</div>
@ -57,54 +59,53 @@
<label class="form-label" for="typeDepreciation" >{{ 'asset-calculator.depreciationMethod' | translate }}</label>
</div>
<div class="col col-lg" >
<select class="form-select" formControlName="typeDepreciation" id="typeDepreciation" >
<select class="form-select" [(ngModel)]="typeDepreciation" id="typeDepreciation" >
<option [ngValue]=TypeDepreciation.linear selected="selected" >{{ 'asset-calculator.linear' | translate }}</option>
<option [ngValue]=TypeDepreciation.digressive >{{ 'asset-calculator.digressive' | translate }}</option>
</select>
</div>
</div>
@if( TypeDepreciation.digressive === assetsDepreciationFormGroup.get( 'typeDepreciation' )?.value ) {
@if( TypeDepreciation.digressive === typeDepreciation() ) {
<div class="form-group row align-items-center">
<div class="col-5 col-lg text-start ">
<label for="factor" class="form-label">{{ 'asset-calculator.degressionCoefficients' | translate }}</label>
</div>
<div class="col col-lg" >
<input type="text" class="form-control" (click)=addChangeValue() formControlName="factor" id="factor" />
<input type="text" class="form-control" [(ngModel)]="factor" id="factor" />
</div>
</div>
}
</form>
}
<div class="col-1">
<input class="btn btn-secondary col-auto" name="addChangeValue"
[disabled]=assetsDepreciationFormGroup.invalid (click)=addChangeValue() type="button" value="{{ 'asset-calculator.addChangeValue' | translate }}">
(click)=addLifeChange() type="button" value="{{ 'asset-calculator.addChangeValue' | translate }}">
</div>
</div>
@if( lifeFormArray.controls.length > 0 ){
@if( lifeChangesSignal().length > 0 ){
<div class="col col-lg-6 mb-2 ms-lg-3">
<div class="row">
<div class="h5 text-center my-1" ><h5 i18n="@@valueChanges">{{ 'asset-calculator.valueChanges' | translate }}</h5></div>
<div class="h5 text-center my-1" ><h5>{{ 'asset-calculator.valueChanges' | translate }}</h5></div>
</div>
<div class="row p-2 ">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th class="col-1 text-center" scope="col-auto">{{ 'asset-calculator.lp' | translate }}</th>
<th class="col-2 text-center" scope="col-auto" i18n="@@month">{{ 'asset-calculator.month' | translate }}</th>
<th class="col-2 text-center" scope="col" i18n="@@change">{{ 'asset-calculator.change' | translate }}</th>
<th class="col-1 text-center" scope="col-auto"> {{ 'asset-calculator.lp' | translate }}</th>
<th class="col-2 text-center" scope="col-auto"> {{ 'asset-calculator.month' | translate }}</th>
<th class="col-2 text-center" scope="col" > {{ 'asset-calculator.change' | translate }}</th>
<th class="col-1 text-center" scope="col"></th>
</tr>
</thead>
<tbody>
@for( changeGroup of lifeFormArray.controls; track $index ) {
<tr [formGroup] = changeGroup >
@for( life of lifeChangesSignal(); track $index ) {
<tr >
<th class="align-middle text-center col-auto" scope="row">{{$index+1}}</th>
<td><input class="form-control" type="month" formControlName="year_month" ></td>
<td><input class="form-control" type="number" formControlName="initialValueAsset" placeholder="Wprowadź wartość {{$index}}"></td>
<td ><button class="btn btn-sm btn-secondary" type="button" (click)=removeChange($index)>{{ 'asset-calculator.remove' | translate }}</button>
<td><input class="form-control" type="month" [(ngModel)]="life.when" ></td>
<td><input class="form-control" type="number" [(ngModel)]="life.initial" placeholder="Wprowadź wartość {{$index}}"></td>
<td><button class="btn btn-sm btn-secondary" type="button" (click)=removeLifeChange($index)>{{ 'asset-calculator.remove' | translate }}</button>
</tr>
}
</tbody>
@ -130,7 +131,7 @@
</tr>
</thead>
<tbody>
@for (position of amortizations; track $index) {
@for (position of amortizationsSignal(); track $index) {
<tr [class]="clazz(position)" class="text-center">
<th scope="row" >{{$index+1}}</th>
<td>{{ position.when.year }}</td>
@ -138,7 +139,7 @@
<td>{{ position.calculatedDepreciation | number:'1.2-2' }}</td>
<td>{{ position.sum | number:'1.2-2' }}</td>
</tr>
@if( 12 === position.when.month || $index === amortizations.length-1){
@if( 12 === position.when.month || $index === amortizationsSignal.length-1){
<tr>
<td colspan="3">
</td>

View File

@ -1,178 +1,219 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ReactiveFormsModule, FormGroup, Validators, FormControl, FormBuilder, AbstractControl } from '@angular/forms';
import { DecimalPipe } from '@angular/common';
import {Asset, AssetPlanPosition, TypeDepreciation, YearMonth, AssetLifeChange, AssetDepreciationMethod, YearMonthUtil } from '../assets/asset';
import {AssetService} from '../assets/service/asset.service'
import {TranslateModule} from "@ngx-translate/core";
import { Component, signal, effect, computed } from '@angular/core';
import { DecimalPipe} from '@angular/common';
import { Asset, AssetPlanPosition, TypeDepreciation, YearMonth, AssetLifeChange, AssetDepreciationMethod } from '../assets/asset';
import { AssetService } from '../assets/service/asset.service';
import { TranslateModule, TranslateService } from "@ngx-translate/core";
import { FormsModule } from '@angular/forms';
interface FormValues {
initialValueAsset: number;
initialValueAsset:number;
rate: number;
startDepreciation: string;
typeDepreciation: TypeDepreciation;
factor: number;
}
@Component({
selector: 'app-asset-calculator',
standalone: true,
imports: [ DecimalPipe, ReactiveFormsModule, TranslateModule ] ,
templateUrl: "asset-calculator.component.html",
styleUrl: 'asset-calculator.component.css'
})
export class AssetCalculatorComponent implements OnInit, OnDestroy{
TypeDepreciation = TypeDepreciation;
class AssetLifeChangeWrapper{
lifeFormArray ;
assetsDepreciationFormGroup : FormGroup;
amortizations : AssetPlanPosition[]=[];
when = signal<string> ( YearMonth.todayTxt());
initial = signal<number>( 0 );
constructor(private fb: FormBuilder,
private assetService : AssetService){
get(): AssetLifeChange {
const ym = this.when();
const when = YearMonth.from( ym );
return new AssetLifeChange( when, this.initial() , 0, 0 );
}
this.assetsDepreciationFormGroup = new FormGroup({
initialValueAsset : new FormControl<number>( 6000, [ Validators.required, this.currencyValidator ]),
rate : new FormControl<number>( 20 ) ,
startDepreciation : new FormControl<string>( new Date().toISOString().slice(0, 7) ),
typeDepreciation : new FormControl<TypeDepreciation>( TypeDepreciation.linear ),
factor : new FormControl<number>( 2/*, [Validators.required,Validators.max(2)]*/ ),
} as {
[key in keyof FormValues]: FormControl<FormValues[key]>
});
this.lifeFormArray = this.fb.array<FormGroup>([]);
constructor( assetLifeChange : AssetLifeChange ){
this.assetsDepreciationFormGroup.valueChanges.subscribe( () => this.calculate() );
this.lifeFormArray.valueChanges.subscribe( () => this.calculate() );
this.when.set( YearMonth.toTxt( assetLifeChange.when ) );
this.initial.set( assetLifeChange.initial );
}
}
ngOnInit(): void{
const savedAsset = localStorage.getItem('assetForCalculator');
if (savedAsset) this.assetToControls(JSON.parse(savedAsset));
this.calculate();
}
ngOnDestroy(): void {
const asset = this.controlsToAsset();
// Zapisywanie danych przed zniszczeniem komponentu
this.saveInStorage( asset );
}
private saveInStorage( asset : Asset ) {
localStorage.setItem('assetForCalculator', JSON.stringify(asset));
}
private assetToControls( asset : Asset ) {
if( asset.life.length > 0 ){
const year_month = YearMonthUtil.toTxt( asset.life[0].when );
this.assetsDepreciationFormGroup.patchValue({
year_month : year_month,
initialValueAsset : asset.life[0].initial,
rate : asset.depreciationMethods[0].rate,
typeDepreciation : asset.depreciationMethods[0].type,
factor : asset.depreciationMethods[0].factor,
})
asset.life.slice(1).forEach( lifeChange => {
this.addChangeValue();
const row = this.lifeFormArray.at( this.lifeFormArray.length - 1 ) as FormGroup;
row.patchValue( {
initialValueAsset: lifeChange.initial,
year_month: YearMonthUtil.toTxt( lifeChange.when )
} );
});
}
}
private controlsToAsset() : Asset {
const formValues = this.assetsDepreciationFormGroup.getRawValue() as FormValues;
const when = new YearMonth( formValues.startDepreciation);
const asset = new Asset( when );
const method = new AssetDepreciationMethod( when.year, formValues.rate, formValues.typeDepreciation, formValues.factor );
asset.addMethod( method );
const creationlifeChange = new AssetLifeChange( when, formValues.initialValueAsset, 0, 0 );
asset.addChange( creationlifeChange );
this.lifeFormArray.controls.forEach(control => {
const initialValue = control.get('initialValueAsset')?.value;
const yearMonth = control.get('year_month')?.value;
asset.addChange( new AssetLifeChange( new YearMonth(yearMonth), initialValue, 0, 0 ) );
})
return asset;
}
calculate(){
const asset = this.controlsToAsset();
this.saveInStorage( asset );
this.assetService.calculate(asset).subscribe(positions => {
this.calculateToValues(positions);
this.amortizations = positions;
});
}
calculateForAsset( asset : Asset ) {
this.assetService.calculate( asset ).subscribe(
positions => {
this.calculateToValues( positions );
this.amortizations = positions });
}
calculateToValues( positions:AssetPlanPosition[] ) {
let sum = 0;
let sumThisYear = 0;
positions.forEach(position => {
position.calculatedDepreciation *= 0.01;
sum += position.calculatedDepreciation;
position.sum = sum;
if (position.when.month === 1) {
sumThisYear = position.calculatedDepreciation;
} else {
sumThisYear += position.calculatedDepreciation;
}
position.sumThisYear = sumThisYear;
});
}
clazz(pos : AssetPlanPosition ){
return pos.when.year % 2 === 0 ? "table-light" : "table-dark";
}
// Walidator dla kwoty
currencyValidator(control: AbstractControl) {
const value = control.value;
const regex = /^\d+(\.\d{1,2})?$/; // Akceptuje liczby z maksymalnie dwoma miejscami po przecinku
return regex.test(value) ? null : { invalidCurrency: true };
}
addChangeValue() {
const formValues = this.assetsDepreciationFormGroup.value as FormValues;
const change = new AssetLifeChange(new YearMonth( formValues.startDepreciation ), 1000, 0, 0 );
const newFormGroup = new FormGroup( { initialValueAsset: new FormControl( change.initial ),
year_month : new FormControl( formValues.startDepreciation )})
this.lifeFormArray.push(newFormGroup);
}
removeChange(index: number) {
this.lifeFormArray.removeAt(index);
}
}
const NAME_IN_STORAGE = 'assetForCalculator';
@Component({
selector: 'app-asset-calculator',
standalone: true,
imports: [DecimalPipe, TranslateModule, FormsModule],
templateUrl: "asset-calculator.component.html",
styleUrls: ['asset-calculator.component.css']
})
export class AssetCalculatorComponent {
TypeDepreciation = TypeDepreciation;
// Signals for form state and amortizations
initialValueAsset = signal <number> ( 6000 );
rate = signal <number> ( 20 );
startDepreciation = signal <string> ( YearMonth.today().toString() );
typeDepreciation = signal <TypeDepreciation>( TypeDepreciation.linear );
factor = signal <number>( 2 );
formValues = computed<FormValues>(() => {
return {
initialValueAsset: this.initialValueAsset(),
rate: this.rate(),
startDepreciation: this.startDepreciation(),
typeDepreciation: this.typeDepreciation(),
factor: this.factor(),
};
});
lifeChangesSignal = signal<AssetLifeChangeWrapper[]>([]); // Replaces `lifeFormArray`
amortizationsSignal =signal<AssetPlanPosition[]>([]);
constructor( private assetService: AssetService,
private translate : TranslateService ) {
// Effect to recalculate when form values change
effect( () => this.reCalculate( ) )
}
ngOnInit(): void{
this.restoreState();
}
ngOnDestroy(): void {
const asset = this.controlsToAsset( );
if( null != asset){
this.saveState( asset );
}
}
private restoreState() {
const savedAsset = localStorage.getItem(NAME_IN_STORAGE);
if( savedAsset ) {
try {
const parsedAsset : Asset = JSON.parse(savedAsset);
this.assetToControls( parsedAsset );
} catch (error) {
console.error('Failed to parse saved asset:', error);
localStorage.removeItem(NAME_IN_STORAGE);
}
}
}
private saveState(asset:Asset) {
// Zapisywanie danych przed zniszczeniem komponentu
try {
// Serialize the asset to a JSON string
localStorage.setItem(NAME_IN_STORAGE, JSON.stringify(asset) );
} catch (error) {
console.error('Failed to save asset:', error);
}
}
private assetToControls( asset : Asset ) {
if( asset.life.length > 0 ){
this.updateBatch(asset);
}
}
private updateBatch(asset: Asset) {
const life0 = asset.life[0];
const method0 = asset.depreciationMethods[0];
this.initialValueAsset.set(life0.initial);
const ymText =YearMonth.toTxt( life0.when );
this.startDepreciation.set(ymText);
this.rate.set(method0.rate);
this.typeDepreciation.set(method0.type);
this.factor.set(method0.rate);
console.log( asset.life );
for( let i = 1; i < asset.life.length ; i++ ){
const assetLifeChange = asset.life[i];
const lifeChangeWrapper = this.addLifeChange2(assetLifeChange);
lifeChangeWrapper.initial.set( assetLifeChange.initial );
lifeChangeWrapper.when.set(YearMonth.toTxt( assetLifeChange.when ));
};
}
private controlsToAsset(): Asset {
const formValues = this.formValues();
const lifeChanges = this.lifeChangesSignal();
const when = YearMonth.from( formValues.startDepreciation );
const asset = new Asset( when );
const method = new AssetDepreciationMethod( when.year, formValues.rate, formValues.typeDepreciation, formValues.factor );
asset.addMethod( method );
const creationLifeChange = new AssetLifeChange( when, formValues.initialValueAsset, 0, 0 );
asset.addChange( creationLifeChange );
lifeChanges.forEach((lifeChange) => {
asset.addChange( lifeChange.get() );
});
return asset;
}
private calculateToValues( positions:AssetPlanPosition[] ) {
let sum = 0;
let sumThisYear = 0;
positions.forEach( position => {
// From gr to zł or from cents to dollars it depends from currency
position.calculatedDepreciation *= 0.01;
sum += position.calculatedDepreciation;
position.sum = sum;
if( position.when.month === 1 ) {
sumThisYear = position.calculatedDepreciation;
} else {
sumThisYear += position.calculatedDepreciation;
}
position.sumThisYear = sumThisYear;
});
}
private reCalculate() {
const asset = this.controlsToAsset( );
this.calculateAmortizationsForAsset(asset);
this.saveState(asset);
}
private calculateAmortizationsForAsset(asset: Asset) {
this.assetService.calculate(asset).subscribe((positions) => {
this.calculateToValues(positions);
this.amortizationsSignal.set(positions);
});
}
clazz(pos : AssetPlanPosition ){
return pos.when.year % 2 === 0 ? "table-light" : "table-dark";
}
// Method to add a life change
addLifeChange( ) {
const assetLifeChange2 : AssetLifeChange
= new AssetLifeChange( YearMonth.from( this.formValues().startDepreciation ), 1000, 0, 0 );
return this.addLifeChange2( assetLifeChange2 );
}
addLifeChange2( assetLifeChange : AssetLifeChange ) {
const newChangeWrapper = new AssetLifeChangeWrapper( assetLifeChange );
this.lifeChangesSignal.update((changes) => [...changes, newChangeWrapper]);
return newChangeWrapper;
}
// Method to remove a life change
removeLifeChange(index: number) {
this.lifeChangesSignal.update((changes) =>
changes.filter((_, i) => i !== index)
);
}
}

View File

@ -5,29 +5,44 @@ export enum TypeDepreciation{
export class YearMonth{
static from( year_month : string) {
const [year, month] = year_month.split('-').map(Number);
return new YearMonth( year, month );
}
readonly year : number ;
readonly month : number ;
constructor( year_month:string ){
const [year, month] = year_month.split('-').map(Number);
constructor( year:number, month : number ){
this.year = year;
this.month = month;
}
}
}
export class YearMonthUtil{
static toTxt( ym : YearMonth ):string{
return ym.year + '-' + ym.month ;
}
static today( ):YearMonth{
const today = new Date();
return new YearMonth( today.getFullYear() + '-' +today.getMonth()+1 );
return new YearMonth( today.getFullYear() , today.getMonth()+1 );
}
}
static todayTxt( ):string{
const today = new Date();
return YearMonth.toTxt ( YearMonth.today());
}
static toTxt( ym : YearMonth ):string{
return ym.year + '-' + String(ym.month).padStart(2, '0');;
}
toJSON() {
return { year: this.year, month: this.month };
}
static fromJSON(json: { year: number; month: number }): YearMonth {
return new YearMonth(json.year, json.month);
}
}
export class AssetLifeChange{
@ -113,7 +128,7 @@ export class AssetsContainer{
}
export class AssetPlanPosition{
when : YearMonth = new YearMonth('0-0');
when : YearMonth = new YearMonth(0,0);
calculatedDepreciation = 0;
sum = 0;
sumThisYear = 0;

View File

@ -1,5 +1,5 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Asset, TypeDepreciation, YearMonthUtil, AssetLifeChange } from '../assets/asset';
import { Asset, TypeDepreciation, YearMonth, AssetLifeChange } from '../assets/asset';
import { ReactiveFormsModule, FormBuilder, FormGroup, FormArray, ValidatorFn, ValidationErrors, AbstractControl } from '@angular/forms';
@ -96,7 +96,7 @@ export class FixedAssetComponent {
const nrInv = control.get('nrInv')?.value;
const initialValue = control.get('initialValue')?.value;
const yearMonth = YearMonthUtil.today();
const yearMonth = YearMonth.today();
const asset = new Asset( yearMonth );
const assetLifeChange = new AssetLifeChange( yearMonth, initialValue, 0, 0 );
asset.addChange( assetLifeChange );