Publish from private repository

This commit is contained in:
Gitea Actions
2026-08-01 12:08:49 +00:00
commit 6f7fd61ee9
695 changed files with 69563 additions and 0 deletions
@@ -0,0 +1,22 @@
<mat-expansion-panel [expanded]="active" (afterExpand)="afterExpand()">
<mat-expansion-panel-header>
<mat-panel-description>
<h4>{{account.account.name}}</h4>
<h4>
{{account.rest | number: '1.2-6'}}
({{account.account.currencyId}})
</h4>
</mat-panel-description>
</mat-expansion-panel-header>
<div>
<app-date-period [from]="dateFrom" [to]="dateTo" (onChange)="onChangePeriod($event)"></app-date-period>
</div>
<hr />
<account-motion [motion]="newMotion" [account]="account.account" (onAdd)="handlerOnAdd($event)">
</account-motion>
<account-motion *ngFor="let motion of motions"
[motion]="motion"
[account]="account.account"
(onDelete)="handlerOnDelete($event)">
</account-motion>
</mat-expansion-panel>
@@ -0,0 +1,15 @@
:host-context(body.dark)account-motion:nth-child(even) {
filter: brightness(1);
}
:host-context(body.dark)account-motion:nth-child(odd) {
filter: brightness(1.75);
}
:host-context(body.light)account-motion:nth-child(even) {
filter: brightness(0.75);
}
:host-context(body.light)account-motion:nth-child(odd) {
filter: brightness(1);
}
@@ -0,0 +1,110 @@
// angular
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Input } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ActivatedRoute } from '@angular/router';
import { Output } from '@angular/core';
import { EventEmitter } from '@angular/core';
// libs
import moment from 'moment';
import { Observable } from 'rxjs';
// app
import { ApiRoutes } from '../../api-routes';
import { AccountDetailedModel } from '../../model/account.detailed.model';
import { MotionModel } from '../../model/motion.model';
@Component({
templateUrl: './account.component.html',
styleUrls: ['./account.component.scss'],
selector: 'account'
})
export class AccountComponent {
public motions?: MotionModel[];
public newMotion: MotionModel;
public dateFrom: Date = moment(new Date).add(-7, 'days').toDate();
public dateTo: Date = moment(new Date).add(0, 'days').toDate();
@Input('account') account!: AccountDetailedModel;
@Input('active') active: boolean = false;
@Input() reloadEvent!: Observable<string[]>;
@Output() onUpdate: EventEmitter<MotionModel[]> = new EventEmitter<MotionModel[]>();
private readonly destroyRef = inject(DestroyRef);
constructor(
private httpClient: HttpClient,
private activatedRoute: ActivatedRoute
) {
this.newMotion = {
date: new Date(),
plus: 0,
minus: 0,
};
}
ngOnInit(): void {
this.loadMotions();
this.reloadEvent
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(x => {
if (x.find(id => id === this.account.account.id)) {
this.loadMotions();
}
});
}
onSubmitClick() {
}
handlerOnAdd(motions: MotionModel[]) {
this.onUpdate?.emit(motions.filter(x => x.accountId !== this.account.account!.id));
this.loadMotions();
this.httpClient
.get<AccountDetailedModel>(ApiRoutes.Account.replace(':id', this.account.account!.id!))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
this.account = data;
});
}
handlerOnDelete(motion: MotionModel) {
const index = this.motions!.findIndex(x => x.id === motion.id);
this.motions!.splice(index, 1);
}
onChangePeriod(dates: Date[]) {
this.dateFrom = dates[0];
this.dateTo = dates[1];
this.loadMotions();
}
private loadMotions() {
var url = ApiRoutes.Motions.replace(':id', this.account.account.id!);
url += '?from=' + moment(this.dateFrom).format('yyyy-MM-DD');
url += '&to=' + moment(this.dateTo).format('yyyy-MM-DD');
this.httpClient
.get<MotionModel[]>(url)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
this.motions = data;
});
}
afterExpand() {
var refresh = window.location.protocol + "//";
refresh += window.location.host;
refresh += window.location.pathname;
refresh += window.location.hash;
var idx = refresh.indexOf('/account/');
if (idx > -1) {
refresh = refresh.substr(0, idx);
}
refresh += '/account/' + this.account.account.id;
window.history.pushState({ path: refresh }, '', refresh);
}
}
@@ -0,0 +1,21 @@
<section class="content">
<div class="content-block">
<div class="block-header">
<!-- breadcrumb -->
<app-breadcrumb [title]="'Blank'" [items]="['Home', 'Accounts']" [active_item]="category">
</app-breadcrumb>
</div>
<div class="row clearfix">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<mat-accordion class="main-headers-align" multi>
<div *ngFor="let account of accounts; let i = index">
<account [account]="account"
[active]="account.account.id === activeAccount"
(onUpdate)="handlerOnUpdate($event)"
[reloadEvent]="reloadSubject.asObservable()" />
</div>
</mat-accordion>
</div>
</div>
</div>
</section>
@@ -0,0 +1,62 @@
// angular
import { Component, DestroyRef, inject, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { ActivatedRoute } from '@angular/router';
// libs
import { Subject } from 'rxjs';
// app
import { AccountCategoryService } from '../../services/account.category.service';
import { ApiRoutes } from '../../api-routes';
import { AccountDetailedModel } from '../../model/account.detailed.model';
import { MotionModel } from '../../model/motion.model';
@Component({
templateUrl: './account.list.component.html',
styleUrls: ['./account.list.component.scss'],
})
export class AccountListComponent implements OnInit {
public accounts?: AccountDetailedModel[];
public category = '';
public activeAccount = '';
reloadSubject: Subject<string[]> = new Subject<string[]>();
private readonly destroyRef = inject(DestroyRef);
constructor(
private httpClient: HttpClient,
private activatedRoute: ActivatedRoute,
private accountCategoryService: AccountCategoryService,
) {
this.activatedRoute.params
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(x => this.activeAccount = x['accountId']);
}
ngOnInit(): void {
this.activatedRoute.params
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(params => {
var categoryId = params['id'];
this.httpClient
.get<AccountDetailedModel[]>(ApiRoutes.Accounts + '?category=' + categoryId)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
this.accounts = data;
});
this.accountCategoryService
.getCategory(categoryId)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
this.category = data.name!;
});
});
}
handlerOnUpdate(motions: MotionModel[]) {
this.reloadSubject.next(motions.map(x => x.accountId!));
}
}
@@ -0,0 +1,84 @@
<form [formGroup]="form" novalidate autocomplete="off" [@pulse]="inProgress">
<div class="" style="display: flex;">
<div class="" style="width: 310px; min-width: 310px;">
<div style="display: flex; align-items: center;">
<a href="javascript:void(0)" matSuffix (click)="addDay(-1)">
<i class="material-icons font-40">arrow_back</i>
</a>
<mat-form-field class="example-full-width" appearance="fill">
<input #dateInput matInput [matDatepicker]="picker3" (focus)="picker3.open()" value={{motion.date}} formControlName="date" required>
<mat-hint>YYYY/MM/DD</mat-hint>
<mat-datepicker-toggle tabindex="-1" matSuffix [for]="picker3"></mat-datepicker-toggle>
<mat-datepicker #picker3></mat-datepicker>
<mat-error *ngIf="form.controls?.['date']?.hasError('required')">
Please enter rate date
</mat-error>
</mat-form-field>
<a href="javascript:void(0)" matSuffix (click)="addDay(1)">
<i class="material-icons font-40">arrow_forward</i>
</a>
</div>
</div>
<div class="" style="flex-grow: 1;">
<mat-form-field class="example-full-width" appearance="fill">
<mat-label>
Motion
<span *ngIf="selectedItem">!!</span>
</mat-label>
<input autocomplete="off" #motionInput matInput formControlName="item" required [matAutocomplete]="auto">
<mat-error *ngIf="form.controls['item'].hasError('required') && !form.pristine">
Please enter motion
</mat-error>
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn" (optionSelected)="selectedFn($event.option.value)">
<mat-option *ngFor="let item of filteredItems" [value]="item">
{{item.name}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field class="example-full-width" appearance="fill">
<input autocomplete="off" matInput formControlName="description">
</mat-form-field>
</div>
<div class="" style="width: 100px; min-width: 100px;">
<mat-form-field class="example-full-width" appearance="fill">
<input matInput [value]="motion.plus | number:'0.2-2'" formControlName="plus" currencyMask [options]="{ prefix: '', precision: 4, inputMode: 1 }">
<mat-error *ngIf="form.hasError('atLeastOne')">
Please enter motion
</mat-error>
</mat-form-field>
<mat-form-field class="example-full-width" appearance="fill">
<input matInput [value]="motion.minus | number:'0.2-2'" formControlName="minus" currencyMask [options]="{ prefix: '', precision: 4, inputMode: 1 }">
<mat-error *ngIf="form.hasError('atLeastOne')">
Please enter motion
</mat-error>
</mat-form-field>
</div>
<div class="" style="display: inline-block; width: 150px; min-width: 150px;">
<button type="button" class="m-2" mat-mini-fab color="primary" *ngIf="!motion.id" (click)="add()">
<i class="material-icons ">add</i>
</button>
<button type="button" class="m-2" mat-mini-fab color="primary" *ngIf="!motion.id" (click)="import()">
<i class="material-icons ">import_export</i>
</button>
<button type="button" class="m-2" mat-mini-fab color="primary" *ngIf="motion.id" (click)="update()">
<i class="material-icons ">save</i>
</button>
<button type="button" class="m-2" mat-mini-fab color="primary" *ngIf="motion.id" (click)="delete()">
<i class="material-icons ">delete</i>
</button>
<div class="preloader m-4" *ngIf="inProgress">
<div class="spinner-layer pl-green">
<div class="circle-clipper left">
<div class="circle"></div>
</div>
<div class="circle-clipper right">
<div class="circle"></div>
</div>
</div>
</div>
</div>
</div>
<hr />
</form>
@@ -0,0 +1,3 @@
.mat-mdc-option.mdc-list-item.mat-mdc-option-active {
filter: brightness(50%)
}
@@ -0,0 +1,274 @@
// angular
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { FormBuilder } from '@angular/forms';
import { FormGroup } from '@angular/forms';
import { Validators } from '@angular/forms';
import { Input } from '@angular/core';
import { Output } from '@angular/core';
import { EventEmitter } from '@angular/core';
import { ViewChild } from '@angular/core';
import { ElementRef } from '@angular/core';
// libs
import moment from 'moment';
import Swal from 'sweetalert2';
import { debounceTime, switchMap } from 'rxjs/operators';
import { of } from 'rxjs';
import { pulseAnimation } from 'angular-animations';
// app
import { ApiRoutes } from '../../api-routes';
import { AccountModel } from '../../model/account.model';
import { MotionModel } from '../../model/motion.model';
import { ItemModel } from '../../model/item.model';
import { atLeastOneNumber } from '../../core/validators/atleastonenumber.validator';
@Component({
selector: 'account-motion',
templateUrl: './motion.component.html',
styleUrls: ['./motion.component.scss'],
animations: [
pulseAnimation({ direction: '<=>', duration: 200 }),
],
})
export class MotionComponent {
public form!: FormGroup;
public errorMessage?: string;
public filteredItems: ItemModel[] = [];
public option?: string;
public selectedItem?: ItemModel;
public inProgress: boolean = false;
@Input('motion') motion!: MotionModel;
@Input('account') account!: AccountModel;
@Output() onAdd: EventEmitter<MotionModel[]> = new EventEmitter<MotionModel[]>();
@Output() onDelete: EventEmitter<MotionModel> = new EventEmitter<MotionModel>();
@ViewChild('motionInput') motionInput?: ElementRef;
@ViewChild('dateInput') dateInput?: ElementRef;
private readonly destroyRef = inject(DestroyRef);
constructor(
private fb: FormBuilder,
private httpClient: HttpClient,
) {
}
ngOnInit(): void {
this.form = this.fb.group({
id: [
this.motion.id,
],
date: [
this.motion.date,
[Validators.required],
],
item: [
this.motion.item,
[Validators.required],
],
description: [
this.motion.description,
],
plus: [
this.motion.plus,
],
minus: [
this.motion.minus,
],
}, {
validator: atLeastOneNumber(Validators.required, ['plus', 'minus'])
});
this.form.controls['item'].valueChanges
.pipe(
debounceTime(200),
switchMap(value => {
if (!value || typeof value !== 'string') {
return of([] as ItemModel[]);
}
this.selectedItem = undefined;
return this.httpClient.get<ItemModel[]>(
ApiRoutes.Items + '?term=' + encodeURIComponent(value)
);
}),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(x => {
this.filteredItems = x;
});
this.form.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.form.controls['plus'].markAsPristine();
this.form.controls['plus'].markAsUntouched();
this.form.controls['plus'].setErrors(null);
this.form.controls['minus'].markAsPristine();
this.form.controls['minus'].markAsUntouched();
this.form.controls['minus'].setErrors(null);
});
}
displayFn(item: any): string {
var value = item && item.name ? item.name : item;
return value;
}
selectedFn(item: ItemModel) {
this.selectedItem = item;
}
add() {
this.form.markAllAsTouched();
if (this.form.hasError('atLeastOne')) {
this.form.get('plus')!.setErrors(this.form.errors);
this.form.get('minus')!.setErrors(this.form.errors);
return;
}
if (!this.form.valid) {
return;
}
this.setInProgress();
var data: MotionModel = {
date: this.form.value.date,
description: this.form.value.description,
plus: this.form.value.plus || 0,
minus: this.form.value.minus || 0,
};
if (this.form.value.item.name) {
data.date = this.form.value.date;
data.item = this.form.value.item.name;
data.itemId = this.form.value.item.id;
data.accountId = this.form.value.item.accountId;
data.amountBalancing = data.plus! > 0 ? data.plus : data.minus;
} else {
data.item = this.form.value.item;
}
this.httpClient
.post<MotionModel[]>(ApiRoutes.Motions.replace(':id', this.account.id!), data)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: response => {
this.onAdd?.emit(response);
this.form.patchValue({
item: '',
plus: 0,
minus: 0,
});
this.filteredItems = [];
this.motionInput?.nativeElement.focus();
this.form.markAsUntouched();
this.setInProgress(false);
},
error: error => {
this.errorMessage = error.detail;
this.setInProgress(false);
}
});
}
private setInProgress(inProgress: boolean = true) {
this.inProgress = inProgress;
}
update() {
this.form.markAllAsTouched();
if (!this.form.valid) {
if (this.form.hasError('atLeastOne')) {
this.form.get('plus')!.setErrors(this.form.errors);
this.form.get('minus')!.setErrors(this.form.errors);
}
return;
}
this.setInProgress();
var url = ApiRoutes.Motion
.replace(':id', this.account.id!)
.replace(':motionId', this.motion.id!);
var data = this.form.value;
data.motion = data.item.name
? data.item.name
: data.motion;
data.itemId = this.selectedItem?.id;
this.httpClient
.put<MotionModel>(url, data)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: () => {
this.form.markAsUntouched();
this.setInProgress(false);
},
error: error => {
this.errorMessage = error.detail;
this.setInProgress(false);
}
});
}
delete() {
Swal.fire({
title: 'Delete motion ' + this.motion.item,
showCancelButton: true,
confirmButtonText: 'Delete',
showLoaderOnConfirm: true,
preConfirm: (name) => {
return new Promise<MotionModel>((resolve, reject) => {
var url = ApiRoutes.Motion
.replace(':id', this.account.id!)
.replace(':motionId', this.motion.id!);
this.setInProgress();
this.httpClient.delete<MotionModel>(url)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: data => {
resolve(data);
this.setInProgress(false);
},
error: error => {
Swal.showValidationMessage(error.detail);
reject();
this.setInProgress(false);
}
});
}).catch(x => {
return false;
});
},
allowOutsideClick: () => !Swal.isLoading(),
}).then((result) => {
if (result.isConfirmed) {
this.onDelete?.emit((result.value! as MotionModel));
}
});
}
import() {
}
addDay(days: number) {
var date = moment(this.form!.get('date')!.value).add(days, 'days').toDate();
this.form.patchValue({
date: date,
});
}
}
@@ -0,0 +1,56 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { UserProfileComponent } from './user-profile/user-profile.component';
import { SettingsCurrencyComponent } from './settings/currency/currency.component';
import { SettingsAccountCategoryComponent } from './settings/account/account.category.component';
import { SettingsAccountComponent } from './settings/account/account.component';
import { SettingsItemCategoryComponent } from './settings/item/item.category.component';
import { SettingsItemComponent } from './settings/item/item.component';
import { AccountListComponent } from './accounts/account.list.component';
const routes: Routes = [
{
path: '',
redirectTo: 'signin',
pathMatch: 'full',
},
{
path: 'user/profile',
component: UserProfileComponent,
},
{
path: 'settings/currencies',
component: SettingsCurrencyComponent,
},
{
path: 'settings/account-categories',
component: SettingsAccountCategoryComponent,
},
{
path: 'settings/accounts',
component: SettingsAccountComponent,
},
{
path: 'settings/item-categories',
component: SettingsItemCategoryComponent,
},
{
path: 'settings/items',
component: SettingsItemComponent,
},
{
path: 'account-category/:id',
component: AccountListComponent,
},
{
path: 'account-category/:id/account/:accountId',
component: AccountListComponent,
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class PagesRoutingModule {
}
+102
View File
@@ -0,0 +1,102 @@
// angular
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';
// libs
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatTabsModule } from '@angular/material/tabs';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatCardModule } from '@angular/material/card';
import { NgxMaskDirective, NgxMaskPipe } from 'ngx-mask';
import { NgxCurrencyDirective } from 'ngx-currency';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatMenuModule } from '@angular/material/menu';
import { MAT_DATE_LOCALE } from '@angular/material/core';
// app
import { PagesRoutingModule } from './pages-routing.module';
import { ComponentsModule } from '../shared/components/components.module';
import { SettingsCurrencyComponent } from './settings/currency/currency.component';
import { SettingsConnectCurrencyComponent } from './settings/currency/connect.currency.component';
import { UserProfileComponent } from './user-profile/user-profile.component';
import { SettingsRateCurrencyComponent } from './settings/currency/rate.currency.component';
import { SettingsAccountCategoryComponent } from './settings/account/account.category.component';
import { SettingsAccountComponent } from './settings/account/account.component';
import { SettingsAccountAddComponent } from './settings/account/add.account.component';
import { SettingsAccountEditComponent } from './settings/account/edit.account.component';
import { SettingsAccountAccessComponent } from './settings/account/access.account.component';
import { CurrencyService } from '../services/currency.service';
import { ItemService } from '../services/item.service';
import { SettingsItemCategoryComponent } from './settings/item/item.category.component';
import { SettingsItemComponent } from './settings/item/item.component';
import { SettingsItemEditComponent } from './settings/item/edit.item.component';
import { AccountListComponent } from './accounts/account.list.component';
import { AccountComponent } from './accounts/account.component';
import { MotionComponent } from './accounts/motion.component';
import { SettingsItemCategoryEditComponent } from './settings/item/edit.item.category.component';
@NgModule({
declarations: [
UserProfileComponent,
SettingsCurrencyComponent,
SettingsConnectCurrencyComponent,
SettingsRateCurrencyComponent,
SettingsAccountCategoryComponent,
SettingsAccountComponent,
SettingsAccountAddComponent,
SettingsAccountEditComponent,
SettingsAccountAccessComponent,
SettingsItemCategoryComponent,
SettingsItemComponent,
SettingsItemEditComponent,
SettingsItemCategoryEditComponent,
AccountListComponent,
AccountComponent,
MotionComponent,
],
imports: [
CommonModule,
ComponentsModule,
FormsModule,
ReactiveFormsModule,
PagesRoutingModule,
MatFormFieldModule,
MatSelectModule,
MatInputModule,
MatIconModule,
MatButtonModule,
MatTabsModule,
MatDialogModule,
MatDatepickerModule,
MatGridListModule,
MatCardModule,
MatExpansionModule,
MatAutocompleteModule,
NgxMaskDirective,
NgxMaskPipe,
NgxCurrencyDirective,
MatCheckboxModule,
MatMenuModule,
],
providers: [
{ provide: MAT_DATE_LOCALE, useValue: 'en-GB' },
CurrencyService,
ItemService,
],
})
export class PagesModule {
}
@@ -0,0 +1,47 @@
<div>
<form [formGroup]="editForm!" (ngSubmit)="onSubmitClick()">
<ng-template #dialogActions>
<button type="button" mat-flat-button class="dialog-btn-cancel" (click)="closeDialog()">Cancel</button>
<button type="submit" class="btn-space" mat-raised-button color="primary">Save</button>
</ng-template>
<app-dialog-chrome title="Account access rights" [errorMessage]="errorMessage" [actions]="dialogActions">
<div class="row">
<div class="col-md-6">
<label>Invite users</label>
<div class="text-inside">
<mat-form-field class="example-full-width">
<mat-label>Email</mat-label>
<input matInput formControlName="email">
</mat-form-field>
</div>
</div>
<div class="col-md-6">
<h6>User rights</h6>
<section>
<mat-checkbox formControlName="allowWrite">
Allow write
</mat-checkbox>
</section>
</div>
</div>
<div formArrayName="accesses">
<div class="row" *ngFor="let access of accessRightsSorted; let i = index" [formGroupName]="i">
<div class="col-md-6">
<input type="hidden" formControlName="userId" />
{{access.user.email}}
<span *ngIf="access.isOwner" class="color-gray">(owner)</span>
</div>
<div class="col-md-3">
<mat-checkbox formControlName="allowWrite">
Allow write
</mat-checkbox>
</div>
<div class="col-md-3">
<button type="button" [disabled]="!access.isAllowDelete" class="btn-space" mat-raised-button color="primary" (click)="removeAccessRight(access)">Delete</button>
</div>
</div>
</div>
</app-dialog-chrome>
</form>
</div>
@@ -0,0 +1,128 @@
// angular
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { Inject } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
// libs
import Swal from 'sweetalert2';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import * as ld from 'lodash';
// app
import { ApiRoutes } from '../../../api-routes';
import { AccountModel } from '../../../model/account.model';
import { MatDialogRef } from '@angular/material/dialog';
import { CurrencyService } from '../../../services/currency.service';
import { CurrencyModel } from '../../../model/currency.model';
import { AccountCategoryService } from '../../../services/account.category.service';
import { AccountCategoryModel } from '../../../model/account.category.model';
import { AccountAccessRightModel } from '../../../model/account.accessRight.model';
import { AccountService } from '../../../services/account.service';
@Component({
templateUrl: './access.account.component.html',
styleUrls: ['./access.account.component.scss'],
})
export class SettingsAccountAccessComponent {
public editForm!: FormGroup;
public errorMessage?: string;
public currencies?: CurrencyModel[];
public categories?: AccountCategoryModel[];
public types = new Map<string, string>();
public accessRightsSorted: AccountAccessRightModel[];
private readonly destroyRef = inject(DestroyRef);
constructor(
private fb: FormBuilder,
private httpClient: HttpClient,
private dialogRef: MatDialogRef<SettingsAccountAccessComponent>,
private accountService: AccountService,
@Inject(MAT_DIALOG_DATA) public account: AccountModel
) {
this.types = this.accountService.getTypes();
this.accessRightsSorted = ld.orderBy(account.accessRights!, ['isOwner', 'user.email'], ['desc', 'asc']);
}
ngOnInit(): void {
const accesses = this.fb.array(
this.accessRightsSorted.map(x => this.fb.group({
userId: [
x.user.id
],
allowWrite: [{
value: x.isAllowWrite,
disabled: !x.isAllowDelete,
}],
}))
);
this.editForm = this.fb.group({
email: [
'',
],
allowWrite: [
false,
],
accesses: accesses,
});
}
removeAccessRight(accessRight: AccountAccessRightModel) {
Swal.fire({
title: 'Delete access ' + accessRight.user.email,
showCancelButton: true,
confirmButtonText: 'Delete',
showLoaderOnConfirm: true,
preConfirm: (name) => {
return new Promise((resolve, reject) => {
var url = ApiRoutes.SettingsAccountAccess
.replace(':id', this.account.id!)
.replace(':access', accessRight.user.id);
this.errorMessage = undefined;
this.httpClient
.delete(url, this.editForm.value)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(response => {
resolve(response);
}, error => {
Swal.showValidationMessage(error.detail);
reject();
});
}).catch(x => {
return false;
});
},
allowOutsideClick: () => !Swal.isLoading(),
}).then((result) => {
if (result.isConfirmed) {
this.dialogRef.close({ refresh: true });
}
});
}
closeDialog(): void {
this.dialogRef.close();
}
onSubmitClick() {
if (this.editForm.valid) {
this.errorMessage = undefined;
this.httpClient
.post<AccountModel>(ApiRoutes.SettingsAccountAccesses.replace(':id', this.account.id!), this.editForm.value)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(response => {
this.dialogRef.close({ refresh: true });
}, error => {
this.errorMessage = error.detail;
});
}
}
}
@@ -0,0 +1,47 @@
<section class="content">
<div class="content-block">
<div class="block-header">
<!-- breadcrumb -->
<app-breadcrumb [title]="'Blank'" [items]="['Home','Settings']" [active_item]="'Account categories'">
</app-breadcrumb>
</div>
<div class="row clearfix">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<mat-card class="card">
<mat-card-header>
<mat-card-title-group>
<mat-card-title>
Account categories
</mat-card-title>
<mat-card-subtitle>
</mat-card-subtitle>
</mat-card-title-group>
<div fxFlex></div>
<button class="btn-space " (click)="add()" mat-raised-button color="primary">
Add
</button>
</mat-card-header>
<mat-card-content>
<div class="body table-responsive">
<table class="table">
<tbody>
<tr *ngFor="let item of categories">
<td>{{item.name}}</td>
<td>
<button class="btn-space" (click)="edit(item)" mat-raised-button color="primary">
Edit
</button>
<button class="btn-space" *ngIf="item.allowDelete" (click)="remove(item)" mat-raised-button color="primary">
Delete
</button>
</td>
</tr>
</tbody>
</table>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
</div>
</section>
@@ -0,0 +1,129 @@
// angular
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
// libs
import Swal from 'sweetalert2';
// app
import { ApiRoutes } from '../../../api-routes';
import { AccountCategoryModel } from '../../../model/account.category.model';
@Component({
templateUrl: './account.category.component.html',
styleUrls: ['./account.category.component.scss'],
})
export class SettingsAccountCategoryComponent {
public categories?: AccountCategoryModel[];
private readonly destroyRef = inject(DestroyRef);
constructor(
private httpClient: HttpClient,
) {
}
ngOnInit(): void {
this.load();
}
private load() {
this.httpClient
.get<AccountCategoryModel[]>(ApiRoutes.SettingsAccountCategories)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
this.categories = data;
});
}
public add() {
Swal.fire({
title: 'Account category name',
input: 'text',
inputAttributes: {
autocapitalize: 'off',
},
showCancelButton: true,
confirmButtonText: 'Add',
showLoaderOnConfirm: true,
preConfirm: (name) => {
return new Promise((resolve, reject) => {
this.httpClient.post(ApiRoutes.SettingsAccountCategories, { name: name })
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
resolve(data);
}, error => {
Swal.showValidationMessage(error.detail);
reject();
});
}).catch(x => {
return false;
});
},
allowOutsideClick: () => !Swal.isLoading(),
}).then((result) => {
this.load();
});
}
public edit(category: AccountCategoryModel) {
Swal.fire({
title: 'Account category name',
input: 'text',
inputValue: category.name,
inputAttributes: {
autocapitalize: 'off',
},
showCancelButton: true,
confirmButtonText: 'Update',
showLoaderOnConfirm: true,
preConfirm: (name) => {
return new Promise((resolve, reject) => {
this.httpClient.put(ApiRoutes.SettingsAccountCategory.replace(':id', category.id!), { name: name })
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
resolve(data);
}, error => {
Swal.showValidationMessage(error.detail);
reject();
});
}).catch(x => {
return false;
});
},
allowOutsideClick: () => !Swal.isLoading(),
}).then((result) => {
this.load();
});
}
public remove(category: AccountCategoryModel) {
Swal.fire({
title: 'Delete account category ' + category.name,
showCancelButton: true,
confirmButtonText: 'Delete',
showLoaderOnConfirm: true,
preConfirm: (name) => {
return new Promise((resolve, reject) => {
this.httpClient.delete(ApiRoutes.SettingsAccountCategory.replace(':id', category.id!))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
resolve(data);
}, error => {
Swal.showValidationMessage(error.detail);
reject();
});
}).catch(x => {
return false;
});
},
allowOutsideClick: () => !Swal.isLoading(),
}).then((result) => {
this.load();
});
}
}
@@ -0,0 +1,99 @@
<section class="content">
<div class="content-block">
<div class="block-header">
<!-- breadcrumb -->
<app-breadcrumb [title]="'Blank'" [items]="['Home','Settings']" [active_item]="'Accounts'">
</app-breadcrumb>
</div>
<div class="row clearfix">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<mat-card class="card">
<mat-card-header>
<mat-card-title-group>
<mat-card-title>
Accounts
</mat-card-title>
<mat-card-subtitle>
</mat-card-subtitle>
</mat-card-title-group>
<div fxFlex></div>
<div *ngIf="invites && invites.length > 0" class="m-r-10">
<button mat-raised-button color="warn" [matMenuTriggerFor]="menu">Invites</button>
<mat-menu #menu="matMenu">
<button *ngFor="let invite of invites" mat-menu-item (click)="startInvite(invite)">{{invite.account}}</button>
</mat-menu>
</div>
<button class="btn-space " (click)="add()" mat-raised-button color="primary">
Add
</button>
</mat-card-header>
<mat-card-content>
<div class="body table-responsive">
<mat-tab-group mat-stretch-tabs="false" mat-align-tabs="start" [(selectedIndex)]="tabIndex">
<mat-tab *ngFor="let category of accountCategories" label="{{category.name}}">
<table class="table">
<tbody>
<tr *ngFor="let account of accountInCategory(category)">
<td>{{account.name}}</td>
<td>{{account.currencyId}}</td>
<td>{{account.type}}</td>
<td>
<button class="btn-space" *ngIf="account.allowWrite" (click)="edit(account)" mat-raised-button color="primary">
Edit
</button>
<button class="btn-space" *ngIf="account.allowManage" (click)="access(account)" mat-raised-button color="primary">
Access
</button>
<button class="btn-space" *ngIf="account.allowDelete" (click)="remove(account)" mat-raised-button color="primary">
Delete
</button>
</td>
</tr>
</tbody>
</table>
</mat-tab>
<mat-tab *ngIf="accountCategories && accounts" label="Without category">
<table class="table">
<tbody>
<tr *ngFor="let account of accountInCategory()">
<td>{{account.name}}</td>
<td>{{account.currencyId}}</td>
<td>{{account.type}}</td>
<td>
<button class="btn-space" (click)="edit(account)" mat-raised-button color="primary">
Edit
</button>
<button class="btn-space" *ngIf="account.allowDelete" (click)="remove(account)" mat-raised-button color="primary">
Delete
</button>
</td>
</tr>
</tbody>
</table>
</mat-tab>
<mat-tab *ngIf="accountCategories && accounts" label="All">
<table class="table">
<tbody>
<tr *ngFor="let account of accounts">
<td>{{account.name}}</td>
<td>{{account.currencyId}}</td>
<td>
<button class="btn-space" (click)="edit(account)" mat-raised-button color="primary">
Edit
</button>
<button class="btn-space" *ngIf="account.allowDelete" (click)="remove(account)" mat-raised-button color="primary">
Delete
</button>
</td>
</tr>
</tbody>
</table>
</mat-tab>
</mat-tab-group>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
</div>
</section>
@@ -0,0 +1,214 @@
// angular
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
// libs
import Swal from 'sweetalert2';
import { MatDialog } from '@angular/material/dialog';
// app
import { ApiRoutes } from '../../../api-routes';
import { AccountModel } from '../../../model/account.model';
import { AccountCategoryModel } from '../../../model/account.category.model';
import { AccountInviteModel } from '../../../model/account.invite.model';
import { SettingsAccountAddComponent } from './add.account.component';
import { SettingsAccountEditComponent } from './edit.account.component';
import { SettingsAccountAccessComponent } from './access.account.component';
import { AccountCategoryService } from '../../../services/account.category.service';
@Component({
templateUrl: './account.component.html',
styleUrls: ['./account.component.scss'],
})
export class SettingsAccountComponent {
public accountCategories?: AccountCategoryModel[];
public accounts?: AccountModel[];
public invites?: AccountInviteModel[];
public tabIndex = 0;
private readonly destroyRef = inject(DestroyRef);
constructor(
private httpClient: HttpClient,
private dialogModel: MatDialog,
private accountCategoryService: AccountCategoryService,
) {
}
ngOnInit(): void {
this.loadCategories();
this.loadInvites();
}
private loadAccounts(accountToShow?: string) {
this.httpClient
.get<AccountModel[]>(ApiRoutes.SettingsAccounts)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
this.accounts = data;
if (accountToShow) {
var idx = -1;
var account = this.accounts.find(x => x.id === accountToShow);
if (account && account.categories && account.categories.length > 0) {
for (var i = 0; i < this.accountCategories!.length; i++) {
if (account!.categories!.find(c => c.id === this.accountCategories![i].id)) {
idx = i;
break;
}
}
}
this.tabIndex = idx === -1 ? this.accountCategories!.length : idx;
}
});
}
private loadInvites() {
return this.httpClient
.get<AccountInviteModel[]>(ApiRoutes.SettingsAccountInvites)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(response => {
this.invites = response;
});
}
private loadCategories() {
this.accountCategoryService
.getCategories()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(x => {
this.accountCategories = x;
this.loadAccounts();
});
}
public accountInCategory(category?: AccountCategoryModel) {
if (this.accounts) {
return this.accounts.filter(acc => (!category && (!acc.categories || acc.categories.length === 0)) || (category && acc.categories && acc.categories.filter(cat => cat.id === category.id).length > 0));
}
return null;
}
public add() {
this.dialogModel.open(SettingsAccountAddComponent, {
width: '640px',
disableClose: true,
data: this.accounts,
}).afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(x => {
if (x && x.refresh) {
this.loadAccounts();
}
});
}
public edit(account: AccountModel) {
this.dialogModel.open(SettingsAccountEditComponent, {
width: '640px',
disableClose: true,
data: account,
}).afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(x => {
if (x && x.refresh) {
this.loadAccounts();
}
});
}
public remove(account: AccountModel) {
Swal.fire({
title: 'Remove account ' + account.name,
showCancelButton: true,
confirmButtonText: 'Delete',
showLoaderOnConfirm: true,
preConfirm: (name) => {
return new Promise((resolve, reject) => {
this.httpClient.delete(ApiRoutes.SettingsAccount.replace(':id', account.id!))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
resolve(data);
}, error => {
Swal.showValidationMessage(error.detail);
reject();
});
}).catch(x => {
return false;
});
},
allowOutsideClick: () => !Swal.isLoading(),
}).then((result) => {
this.loadAccounts();
});
}
public access(account: AccountModel) {
this.dialogModel.open(SettingsAccountAccessComponent, {
width: '640px',
disableClose: true,
data: account,
}).afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(x => {
if (x && x.refresh) {
this.loadAccounts();
}
});
}
public startInvite(invite: AccountInviteModel) {
Swal.fire({
title: 'Accept to invite the account "' + invite.account + '"',
input: 'text',
inputLabel: 'Accept with name',
inputValue: invite.account,
showDenyButton: true,
showCancelButton: true,
confirmButtonText: 'Accept',
denyButtonText: 'Reject',
showLoaderOnConfirm: true,
preConfirm: (input) => {
return new Promise((resolve, reject) => {
this.httpClient.post(ApiRoutes.SettingsAccountInviteAccept.replace(':id', invite.id!), { name: input })
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
resolve(data);
}, error => {
Swal.showValidationMessage(error);
reject();
});
}).catch(x => {
return false;
});
},
preDeny: (x) => {
return new Promise((resolve, reject) => {
this.httpClient.post(ApiRoutes.SettingsAccountInviteReject.replace(':id', invite.id!), null)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
resolve(data);
}, error => {
Swal.showValidationMessage(error.detail);
reject();
});
}).catch(x => {
return false;
});
},
allowOutsideClick: () => !Swal.isLoading(),
}).then((result) => {
var id;
if (result.value) {
var account = result.value as AccountModel;
id = account.id;
}
this.loadInvites();
this.loadAccounts(id);
});
}
}
@@ -0,0 +1,62 @@
<div>
<form [formGroup]="editForm!" (ngSubmit)="onSubmitClick()">
<ng-template #dialogActions>
<button type="button" mat-flat-button class="dialog-btn-cancel" (click)="closeDialog()">Cancel</button>
<button type="submit" class="btn-space" mat-raised-button color="primary">Save</button>
</ng-template>
<app-dialog-chrome title="Add account" [errorMessage]="errorMessage" [actions]="dialogActions">
<div class="row">
<div class="col-md-12">
<div class="text-inside">
<mat-form-field class="example-full-width">
<mat-label>Type</mat-label>
<mat-select formControlName="type">
<mat-option *ngFor="let type of types | keyvalue" [value]="type.key">
{{type.key}} ({{type.value}})
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="text-inside">
<mat-form-field class="example-full-width">
<mat-label>Name</mat-label>
<input matInput value={{account.name}} formControlName="name">
</mat-form-field>
</div>
</div>
<div class="col-md-6">
<div class="text-inside">
<mat-form-field class="example-full-width">
<mat-label>Currency</mat-label>
<mat-select formControlName="currencyId">
<mat-option *ngFor="let currency of currencies" [value]="currency.code">
{{currency.code}} ({{currency.name}})
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="text-inside">
<mat-form-field class="example-full-width">
<mat-label>Category</mat-label>
<mat-select formControlName="categoryId">
<mat-option *ngFor="let category of categories" [value]="category.id">
{{category.name}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
</app-dialog-chrome>
</form>
</div>
@@ -0,0 +1,101 @@
// angular
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { Inject } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
// libs
import Swal from 'sweetalert2';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
// app
import { ApiRoutes } from '../../../api-routes';
import { AccountModel } from '../../../model/account.model';
import { MatDialogRef } from '@angular/material/dialog';
import { CurrencyService } from '../../../services/currency.service';
import { CurrencyModel } from '../../../model/currency.model';
import { AccountCategoryService } from '../../../services/account.category.service';
import { AccountCategoryModel } from '../../../model/account.category.model';
import { AuthService } from '../../../core/service/auth.service';
import { AccountService } from '../../../services/account.service';
@Component({
templateUrl: './add.account.component.html',
styleUrls: ['./add.account.component.scss'],
})
export class SettingsAccountAddComponent {
public editForm!: FormGroup;
public errorMessage?: string;
public currencies?: CurrencyModel[];
public categories?: AccountCategoryModel[];
public username: string;
public types = new Map<string, string>();
private readonly destroyRef = inject(DestroyRef);
constructor(
private fb: FormBuilder,
private httpClient: HttpClient,
private dialogRef: MatDialogRef<SettingsAccountAddComponent>,
private currencyService: CurrencyService,
private accountCategoryService: AccountCategoryService,
private authService: AuthService,
private accountService: AccountService,
@Inject(MAT_DIALOG_DATA) public account: AccountModel
) {
this.username = authService.currentUserValue.userName;
this.types = this.accountService.getTypes();
}
ngOnInit(): void {
this.editForm = this.fb.group({
id: [
this.account.id,
],
name: [
this.account.name,
[Validators.required],
],
currencyId: [
this.account.currencyId,
[Validators.required],
],
categoryId: [
'',
[Validators.required],
],
type: [
this.account.type
]
});
this.currencyService
.getCurrencies()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(x => this.currencies = x);
this.accountCategoryService
.getCategories()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(x => this.categories = x);
}
closeDialog(): void {
this.dialogRef.close();
}
onSubmitClick() {
if (this.editForm.valid) {
this.errorMessage = undefined;
this.httpClient
.post<AccountModel>(ApiRoutes.SettingsAccounts, this.editForm.value)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(response => {
this.dialogRef.close({ refresh: true });
}, error => {
this.errorMessage = error.detail;
});
}
}
}
@@ -0,0 +1,93 @@
<div>
<form [formGroup]="editForm!" (ngSubmit)="onSubmitClick()">
<ng-template #dialogActions>
<button type="button" mat-flat-button class="dialog-btn-cancel" (click)="closeDialog()">Cancel</button>
<button type="submit" class="btn-space" mat-raised-button color="primary">Save</button>
</ng-template>
<app-dialog-chrome title="Edit account" [errorMessage]="errorMessage" [actions]="dialogActions">
<div class="row">
<div class="col-md-12">
<div class="text-inside">
<mat-form-field class="example-full-width">
<mat-label>Type</mat-label>
<mat-select formControlName="type">
<mat-option *ngFor="let type of types | keyvalue" [value]="type.key">
{{type.key}} ({{type.value}})
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="text-inside">
<mat-form-field class="example-full-width">
<mat-label>Name</mat-label>
<input matInput value={{account.name}} formControlName="name">
</mat-form-field>
</div>
</div>
<div class="col-md-6">
<div class="text-inside">
<mat-form-field class="example-full-width">
<mat-label>Currency</mat-label>
<mat-select formControlName="currencyId">
<mat-option *ngFor="let currency of currencies" [value]="currency.code">
{{currency.code}} ({{currency.name}})
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<label class="section-label">Categories</label>
<table class="table" style="table-layout: fixed;" *ngIf="categoriesSorted?.length; else noCategories">
<tbody>
<tr *ngFor="let category of categoriesSorted">
<td style="width: 100%;">{{category.name}}</td>
<td style="width: 100px;">
<button type="button" class="btn-space" (click)="removeCategory(category)" mat-raised-button color="warn">
Delete
</button>
</td>
</tr>
</tbody>
</table>
<ng-template #noCategories>
<div class="empty-categories">No categories assigned</div>
</ng-template>
</div>
</div>
<div class="row category-add-block">
<div class="col-md-12">
<label class="section-label">Add category</label>
<div class="category-add-row">
<mat-form-field class="example-full-width category-add-select">
<mat-label>Category</mat-label>
<mat-select formControlName="categoryId">
<mat-option *ngFor="let category of availableCategories" [value]="category.id">
{{category.name}}
</mat-option>
</mat-select>
</mat-form-field>
<button type="button"
class="btn-space"
mat-raised-button
color="primary"
[disabled]="!editForm.get('categoryId')?.value"
(click)="addCategory()">
Add
</button>
</div>
</div>
</div>
</app-dialog-chrome>
</form>
</div>
@@ -0,0 +1,24 @@
.section-label {
display: block;
font-weight: 500;
margin-bottom: 8px;
}
.category-add-block {
margin-bottom: 8px;
}
.category-add-row {
display: flex;
align-items: flex-start;
gap: 12px;
}
.category-add-select {
flex: 1;
}
.empty-categories {
opacity: 0.7;
padding: 8px 0 16px;
}
@@ -0,0 +1,161 @@
// angular
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { Inject } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
// libs
import Swal from 'sweetalert2';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import * as ld from 'lodash';
// app
import { ApiRoutes } from '../../../api-routes';
import { AccountModel } from '../../../model/account.model';
import { MatDialogRef } from '@angular/material/dialog';
import { CurrencyService } from '../../../services/currency.service';
import { CurrencyModel } from '../../../model/currency.model';
import { AccountCategoryService } from '../../../services/account.category.service';
import { AccountCategoryModel } from '../../../model/account.category.model';
import { AccountAccessRightModel } from '../../../model/account.accessRight.model';
import { AccountService } from '../../../services/account.service';
@Component({
templateUrl: './edit.account.component.html',
styleUrls: ['./edit.account.component.scss'],
})
export class SettingsAccountEditComponent {
public editForm!: FormGroup;
public errorMessage?: string;
public currencies?: CurrencyModel[];
public categories?: AccountCategoryModel[];
public types = new Map<string, string>();
public categoriesSorted: AccountCategoryModel[];
public accessRightsSorted: AccountAccessRightModel[];
private readonly destroyRef = inject(DestroyRef);
constructor(
private fb: FormBuilder,
private httpClient: HttpClient,
private dialogRef: MatDialogRef<SettingsAccountEditComponent>,
private currencyService: CurrencyService,
private accountCategoryService: AccountCategoryService,
private accountService: AccountService,
@Inject(MAT_DIALOG_DATA) public account: AccountModel
) {
this.types = this.accountService.getTypes();
this.categoriesSorted = ld.orderBy(account.categories ?? [], ['name'], ['asc']);
this.accessRightsSorted = ld.orderBy(account.accessRights!, ['isOwner', 'user.email'], ['desc', 'asc']);
}
/** Categories not yet assigned to this account (for the Add select). */
get availableCategories(): AccountCategoryModel[] {
const assigned = new Set((this.categoriesSorted ?? []).map(c => c.id));
return (this.categories ?? []).filter(c => !assigned.has(c.id));
}
ngOnInit(): void {
this.editForm = this.fb.group({
id: [
this.account.id,
],
name: [
this.account.name,
[Validators.required],
],
currencyId: [
this.account.currencyId,
[Validators.required],
],
categoryId: [
'',
],
type: [
this.account.type,
],
userEmail: [
'',
],
});
this.currencyService
.getCurrencies()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(x => this.currencies = x);
this.accountCategoryService
.getCategories()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(x => this.categories = x);
}
addCategory() {
const categoryId = this.editForm.get('categoryId')?.value;
if (!categoryId || !this.editForm.valid) {
this.editForm.markAllAsTouched();
return;
}
this.errorMessage = undefined;
this.httpClient
.put<AccountModel>(ApiRoutes.SettingsAccount.replace(':id', this.account.id!), this.editForm.value)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: account => {
this.applyAccountCategories(account);
this.editForm.patchValue({ categoryId: '' });
},
error: error => {
this.errorMessage = error.detail;
},
});
}
removeCategory(category: AccountCategoryModel) {
const url = ApiRoutes.SettingsAccountAccountCategory
.replace(':id', this.account.id!)
.replace(':categoryId', category.id!);
this.errorMessage = undefined;
this.httpClient
.delete<AccountModel>(url)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: account => this.applyAccountCategories(account),
error: error => {
this.errorMessage = error.detail;
},
});
}
private applyAccountCategories(account: AccountModel) {
this.account.categories = account.categories ?? [];
this.categoriesSorted = ld.orderBy(this.account.categories, ['name'], ['asc']);
}
removeAccessRight(accessRight: AccountAccessRightModel) {
}
closeDialog(): void {
this.dialogRef.close();
}
onSubmitClick() {
if (this.editForm.valid) {
this.errorMessage = undefined;
this.httpClient
.put<AccountModel>(ApiRoutes.SettingsAccount.replace(':id', this.account.id!), this.editForm.value)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(response => {
this.dialogRef.close({ refresh: true });
}, error => {
this.errorMessage = error.detail;
});
}
}
}
@@ -0,0 +1,85 @@
<div>
<form [formGroup]="addForm!" (ngSubmit)="onSubmitClick()">
<ng-template #dialogActions>
<button type="button" mat-flat-button class="dialog-btn-cancel" (click)="closeDialog()">Cancel</button>
<button type="submit" class="btn-space" mat-raised-button color="primary">Save</button>
</ng-template>
<app-dialog-chrome title="Add currency" [errorMessage]="errorMessage" [actions]="dialogActions">
<div class="row">
<div class="col-md-6">
<div class="text-inside">
<mat-form-field class="example-full-width">
<mat-label>Symbol</mat-label>
<input matInput value={{currency.symbol}} formControlName="symbol" readonly="">
</mat-form-field>
</div>
</div>
<div class="col-md-6">
<div class="text-inside">
<mat-form-field class="example-full-width">
<mat-label>Code</mat-label>
<input matInput value={{currency.id}} formControlName="id" readonly="">
</mat-form-field>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="text-inside">
<mat-form-field class="example-full-width">
<mat-label>Name</mat-label>
<input matInput value={{currency.name}} formControlName="name">
</mat-form-field>
</div>
</div>
<div class="col-md-6">
<div class="text-inside">
<mat-form-field class="example-full-width">
<mat-label>Short Name</mat-label>
<input matInput value={{currency.shortName}} formControlName="shortName">
</mat-form-field>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="text-inside">
<mat-form-field class="example-full-width" appearance="fill">
<mat-label>Rate</mat-label>
<input matInput [value]="currency.rate | number:'0.2-2'" formControlName="rate" currencyMask [options]="{ prefix: currency.symbol, precision: 4 }" required>
<mat-error *ngIf="addForm.controls?.['rate']?.hasError('required')">
Please enter rate
</mat-error>
</mat-form-field>
</div>
</div>
<div class="col-md-4">
<div class="text-inside">
<mat-form-field class="example-full-width" appearance="fill">
<mat-label>Quantity</mat-label>
<input matInput [value]="currency.rate | number:'0.0-0'" formControlName="quantity" currencyMask [options]="{ prefix: '', precision: 0 }" required>
<mat-error *ngIf="addForm.controls?.['rate']?.hasError('required')">
Please enter rate
</mat-error>
</mat-form-field>
</div>
</div>
<div class="col-md-4">
<div class="text-inside">
<mat-form-field class="example-full-width">
<mat-label>Rate Date</mat-label>
<input matInput [matDatepicker]="picker3" (focus)="picker3.open()" value={{currency.rateDate}} formControlName="rateDate" required>
<mat-hint>YYYY/MM/DD</mat-hint>
<mat-datepicker-toggle matSuffix [for]="picker3"></mat-datepicker-toggle>
<mat-datepicker #picker3></mat-datepicker>
<mat-error *ngIf="addForm.controls?.['rateDate']?.hasError('required')">
Please enter rate date
</mat-error>
</mat-form-field>
</div>
</div>
</div>
</app-dialog-chrome>
</form>
</div>
@@ -0,0 +1,80 @@
// angular
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
// libs
import { MatDialogRef } from '@angular/material/dialog';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
// app
import { ApiRoutes } from '../../../api-routes';
import { CurrencyModel } from '../../../model/currency.model';
@Component({
templateUrl: './connect.currency.component.html',
styleUrls: ['./connect.currency.component.scss'],
})
export class SettingsConnectCurrencyComponent {
public addForm!: FormGroup;
public errorMessage?: string;
private readonly destroyRef = inject(DestroyRef);
constructor(
private fb: FormBuilder,
private httpClient: HttpClient,
private dialogRef: MatDialogRef<SettingsConnectCurrencyComponent>,
@Inject(MAT_DIALOG_DATA) public currency: CurrencyModel
) {
}
public ngOnInit(): void {
this.addForm = this.fb.group({
id: [
this.currency.id,
],
symbol: [
this.currency.symbol,
],
name: [
this.currency.name,
],
shortName: [
this.currency.id,
],
quantity: [
this.currency.quantity || 1,
[Validators.required],
],
rate: [
this.currency.rate || 1,
[Validators.required],
],
rateDate: [
this.currency.rateDate,
[Validators.required],
],
});
}
closeDialog(): void {
this.dialogRef.close();
}
onSubmitClick() {
if (this.addForm.valid) {
this.errorMessage = undefined;
this.httpClient
.post<CurrencyModel[]>(ApiRoutes.SettingsCurrencies, this.addForm.value)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(response => {
this.dialogRef.close({ refresh: true });
}, error => {
this.errorMessage = error.detail;
});
}
}
}
@@ -0,0 +1,88 @@
<section class="content">
<div class="content-block">
<div class="block-header">
<!-- breadcrumb -->
<app-breadcrumb [title]="'Blank'" [items]="['Home','Settings']" [active_item]="'Currency'">
</app-breadcrumb>
</div>
<div class="row clearfix">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<div class="card">
<div class="header">
<h2><strong>My currencies</strong></h2>
</div>
<div class="body">
<mat-tab-group [(selectedIndex)]="tabIndex">
<mat-tab label="My currencies">
<div class="body table-responsive">
<table class="table">
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>Short Name</th>
<th>Quantity</th>
<th>Rate</th>
<th>Rate Date</th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of myCurrencies" [class.currency-row-primary]="item.isPrimary">
<td>{{item.code}}</td>
<td>{{item.name}}</td>
<td>{{item.shortName}}</td>
<td style="text-align: end">
{{item.quantity}}
</td>
<td style="text-align: end">
{{item.rate | number: '1.2-6'}}
{{item.symbol}}
</td>
<td>{{item.rateDate | date:'yyyy-MM-dd'}}</td>
<td>
<button class="btn-space" (click)="setRate(item)" mat-raised-button color="primary">
Edit
</button>
<button class="btn-space" (click)="delete(item)" mat-raised-button color="warn">
Delete
</button>
</td>
</tr>
</tbody>
</table>
</div>
</mat-tab>
<mat-tab label="All currencies">
<div class="body table-responsive">
<table class="table">
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of currencies">
<td>{{item.symbol}}</td>
<td>{{item.id}}</td>
<td>{{item.name}}</td>
<td>
<button class="btn-space" (click)="connect(item)" [disabled]="item.isConnected" mat-raised-button color="primary">
Add
</button>
</td>
</tr>
</tbody>
</table>
</div>
</mat-tab>
</mat-tab-group>
</div>
</div>
</div>
</div>
</div>
</section>
@@ -0,0 +1,15 @@
// Primary currency highlight must target cells — Bootstrap paints td backgrounds,
// so a class on <tr> alone is invisible in light theme.
.table tbody tr.currency-row-primary > td,
.table tbody tr.currency-row-primary > th {
background-color: rgba(0, 188, 212, 0.18) !important;
color: inherit;
}
:host-context(body.dark) {
.table tbody tr.currency-row-primary > td,
.table tbody tr.currency-row-primary > th {
background-color: rgba(33, 150, 243, 0.22) !important;
color: #cfe8ff;
}
}
@@ -0,0 +1,133 @@
// angular
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
// libs
import { MatDialog } from '@angular/material/dialog';
import Swal from 'sweetalert2';
// app
import { ApiRoutes } from '../../../api-routes';
import { SettingsConnectCurrencyComponent } from './connect.currency.component';
import { CurrencyModel } from '../../../model/currency.model';
import { SettingsRateCurrencyComponent } from './rate.currency.component';
@Component({
templateUrl: './currency.component.html',
styleUrls: ['./currency.component.scss'],
})
export class SettingsCurrencyComponent {
public tabIndex = 0;
public currencies?: CurrencyModel[];
public myCurrencies?: CurrencyModel[];
public primaryId?: string;
private readonly destroyRef = inject(DestroyRef);
constructor(
private httpClient: HttpClient,
private dialogModel: MatDialog
) {
}
ngOnInit(): void {
this.load();
}
connect(currency: CurrencyModel) {
this.dialogModel.open(SettingsConnectCurrencyComponent, {
width: '640px',
disableClose: true,
data: currency,
}).afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(x => {
if (x && x.refresh) {
this.tabIndex = 0;
this.loadMyCurrency();
}
});
}
disconnect(currency: CurrencyModel) {
}
setRate(currency: CurrencyModel) {
this.dialogModel.open(SettingsRateCurrencyComponent, {
width: '640px',
disableClose: true,
data: currency,
}).afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(x => {
if (x && x.refresh) {
this.loadMyCurrency();
}
});
}
delete(currency: CurrencyModel) {
Swal.fire({
title: 'Delete currency ' + currency.name,
showCancelButton: true,
confirmButtonText: 'Delete',
showLoaderOnConfirm: true,
preConfirm: (name) => {
return new Promise((resolve, reject) => {
this.httpClient.delete(ApiRoutes.SettingsCurrency.replace(':id', currency.id!))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
resolve(data);
}, error => {
Swal.showValidationMessage(error.detail);
reject();
});
}).catch(x => {
return false;
});
},
allowOutsideClick: () => !Swal.isLoading(),
}).then((result) => {
this.load();
});
}
private load() {
this.httpClient
.get<CurrencyModel[]>(ApiRoutes.GeneralCurrencies)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
this.currencies = data;
this.loadMyCurrency();
});
}
private loadMyCurrency() {
this.httpClient
.get<CurrencyModel[]>(ApiRoutes.SettingsCurrencies)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
this.myCurrencies = data
.sort((a, b) => a.code!.localeCompare(b.code!))
.map(myCurrency => {
var globalCurrency = this.currencies?.find(x => x.id === myCurrency.code);
return {
id: myCurrency.id,
code: myCurrency.code,
name: myCurrency.name,
quantity: myCurrency.quantity,
rate: myCurrency.rate,
rateDate: myCurrency.rateDate,
shortName: myCurrency.shortName,
symbol: globalCurrency?.symbol,
isPrimary: myCurrency.isPrimary,
};
});
});
}
}
@@ -0,0 +1,95 @@
<div>
<form [formGroup]="addForm!" (ngSubmit)="onSubmitClick()">
<ng-template #dialogActions>
<button type="button" mat-flat-button class="dialog-btn-cancel" (click)="closeDialog()">Cancel</button>
<button type="submit" class="btn-space" mat-raised-button color="primary">Save</button>
</ng-template>
<app-dialog-chrome title="Edit currency" [error]="error" [actions]="dialogActions">
<div class="row">
<div class="col-md-3">
<div class="text-inside">
<mat-form-field class="example-full-width">
<mat-label>Symbol</mat-label>
<input matInput value={{currency.symbol}} formControlName="symbol" readonly="">
</mat-form-field>
</div>
</div>
<div class="col-md-3">
<div class="text-inside">
<mat-form-field class="example-full-width">
<mat-label>Code</mat-label>
<input matInput value={{currency.code}} formControlName="code" readonly="">
</mat-form-field>
</div>
</div>
<div class="col-md-3">
<div class="text-inside">
<mat-form-field class="example-full-width">
<mat-label>Name</mat-label>
<input matInput value={{currency.name}} formControlName="name">
</mat-form-field>
</div>
</div>
<div class="col-md-3">
<div class="text-inside">
<mat-form-field class="example-full-width">
<mat-label>Short name</mat-label>
<input matInput value={{currency.shortName}} formControlName="shortName">
</mat-form-field>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="text-inside">
<mat-form-field class="example-full-width" appearance="fill">
<mat-label>Rate</mat-label>
<input matInput [value]="currency.rate | number:'0.2-2'" formControlName="rate" currencyMask
[options]="{ prefix: '', precision: 4, inputMode: 1 }" required>
<mat-error *ngIf="addForm.controls?.['rate']?.hasError('required')">
Please enter rate
</mat-error>
</mat-form-field>
</div>
</div>
<div class="col-md-4">
<div class="text-inside">
<mat-form-field class="example-full-width" appearance="fill">
<mat-label>Quantity</mat-label>
<input matInput [value]="currency.rate | number:'0.0-0'" formControlName="quantity"
currencyMask [options]="{ prefix: '', precision: 0 }" required>
<mat-error *ngIf="addForm.controls?.['rate']?.hasError('required')">
Please enter rate
</mat-error>
</mat-form-field>
</div>
</div>
<div class="col-md-4">
<div class="text-inside">
<mat-form-field class="example-full-width">
<mat-label>Rate Date</mat-label>
<input matInput [matDatepicker]="picker3" (focus)="picker3.open()"
value={{currency.rateDate}} formControlName="rateDate" required>
<mat-hint>DD/MM/YYYY</mat-hint>
<mat-datepicker-toggle matSuffix [for]="picker3"></mat-datepicker-toggle>
<mat-datepicker #picker3></mat-datepicker>
<mat-error *ngIf="addForm.controls?.['rateDate']?.hasError('required')">
Please enter rate date
</mat-error>
</mat-form-field>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="text-inside">
<mat-checkbox class="example-margin" formControlName="isPrimary">
Primary currency
</mat-checkbox>
</div>
</div>
</div>
</app-dialog-chrome>
</form>
</div>
@@ -0,0 +1,99 @@
// angular
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
// libs
import { MatDialogRef } from '@angular/material/dialog';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
// app
import { ApiRoutes } from '../../../api-routes';
import { CurrencyModel } from '../../../model/currency.model';
@Component({
templateUrl: './rate.currency.component.html',
styleUrls: ['./rate.currency.component.scss'],
})
export class SettingsRateCurrencyComponent {
public addForm!: FormGroup;
public error?: any;
private readonly destroyRef = inject(DestroyRef);
constructor(
private fb: FormBuilder,
private httpClient: HttpClient,
private dialogRef: MatDialogRef<SettingsRateCurrencyComponent>,
@Inject(MAT_DIALOG_DATA) public currency: CurrencyModel
) {
}
public ngOnInit(): void {
this.addForm = this.fb.group({
id: [
this.currency.id,
],
code: [
this.currency.code,
],
symbol: [
this.currency.symbol,
],
name: [
this.currency.name,
],
shortName: [
this.currency.shortName,
],
quantity: [
this.currency.quantity || 1,
[Validators.required],
],
rate: [
this.currency.rate || 1,
[Validators.required],
],
rateDate: [
this.currency.rateDate,
[Validators.required],
],
isPrimary: [
this.currency.isPrimary,
],
});
}
closeDialog(): void {
this.dialogRef.close();
}
onSubmitClick() {
this.error = undefined;
if (this.addForm.valid) {
this.httpClient
.put<CurrencyModel>(ApiRoutes.SettingsCurrency.replace(':id', this.addForm.value.id), this.addForm.value)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: (response) => {
this.httpClient
.post<CurrencyModel[]>(ApiRoutes.SettingsCurrenciesRate.replace(':id', this.addForm.value.id), this.addForm.value)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: (response) => {
this.dialogRef.close({ refresh: true });
},
error: (error) => {
this.error = error;
},
});
},
error: (error) => {
this.error = error;
}
});
}
}
}
@@ -0,0 +1,26 @@
<div>
<form [formGroup]="editForm!" (ngSubmit)="onSubmitClick()">
<ng-template #dialogActions>
<button type="button" mat-flat-button class="dialog-btn-cancel" (click)="closeDialog()">Cancel</button>
<button type="submit" class="btn-space" mat-raised-button color="primary">Save</button>
</ng-template>
<app-dialog-chrome title="Edit item category" [errorMessage]="errorMessage" [actions]="dialogActions">
<div class="row">
<div class="col-md-6">
<div class="text-inside">
<mat-form-field class="example-full-width">
<mat-label>Name</mat-label>
<input matInput value={{category.name}} formControlName="name">
</mat-form-field>
</div>
</div>
<div class="col-md-6">
<div class="text-inside">
<mat-checkbox class="example-margin" formControlName="internal">Is internal motion</mat-checkbox>
</div>
</div>
</div>
</app-dialog-chrome>
</form>
</div>
@@ -0,0 +1,77 @@
// angular
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { Inject } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
// libs
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatDialogRef } from '@angular/material/dialog';
// app
import { ApiRoutes } from '../../../api-routes';
import { ItemCategoryModel } from '../../../model/item.category.model';
@Component({
templateUrl: './edit.item.category.component.html',
styleUrls: ['./edit.item.category.component.scss'],
})
export class SettingsItemCategoryEditComponent {
public editForm!: FormGroup;
public errorMessage?: string;
private readonly destroyRef = inject(DestroyRef);
constructor(
private fb: FormBuilder,
private httpClient: HttpClient,
private dialogRef: MatDialogRef<SettingsItemCategoryEditComponent>,
@Inject(MAT_DIALOG_DATA) public category: ItemCategoryModel
) {
}
ngOnInit(): void {
this.editForm = this.fb.group({
id: [
this.category.id,
],
name: [
this.category.name,
[Validators.required],
],
internal: [
this.category?.internal ?? false,
],
});
}
closeDialog(): void {
this.dialogRef.close();
}
onSubmitClick() {
if (this.editForm.valid) {
this.errorMessage = undefined;
if (!this.category.id) {
this.httpClient
.post<ItemCategoryModel>(ApiRoutes.SettingsItemCategories, this.editForm.value)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(response => {
this.dialogRef.close({ category: response, refresh: true });
}, error => {
this.errorMessage = error.detail;
});
} else {
this.httpClient
.put<ItemCategoryModel>(ApiRoutes.SettingsItemCategory.replace(':id', this.category.id!), this.editForm.value)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(response => {
this.dialogRef.close({ category: response, refresh: true });
}, error => {
this.errorMessage = error.detail;
});
}
}
}
}
@@ -0,0 +1,33 @@
<div>
<form [formGroup]="editForm!" (ngSubmit)="onSubmitClick()">
<ng-template #dialogActions>
<button type="button" mat-flat-button class="dialog-btn-cancel" (click)="closeDialog()">Cancel</button>
<button type="submit" class="btn-space" mat-raised-button color="primary">Save</button>
</ng-template>
<app-dialog-chrome title="Edit motion" [errorMessage]="errorMessage" [actions]="dialogActions">
<div class="row">
<div class="col-md-6">
<div class="text-inside">
<mat-form-field class="example-full-width">
<mat-label>Name</mat-label>
<input matInput value={{item.name}} formControlName="name" readonly>
</mat-form-field>
</div>
</div>
<div class="col-md-6">
<div class="text-inside">
<mat-form-field class="example-full-width">
<mat-label>Category</mat-label>
<mat-select formControlName="category">
<mat-option *ngFor="let category of categories" [value]="category.id">
{{category.name}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
</app-dialog-chrome>
</form>
</div>
@@ -0,0 +1,78 @@
// angular
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { Inject } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
// libs
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatDialogRef } from '@angular/material/dialog';
// app
import { ApiRoutes } from '../../../api-routes';
import { ItemCategoryModel } from '../../../model/item.category.model';
import { ItemModel } from '../../../model/item.model';
import { ItemService } from '../../../services/item.service';
@Component({
templateUrl: './edit.item.component.html',
styleUrls: ['./edit.item.component.scss'],
})
export class SettingsItemEditComponent {
public editForm!: FormGroup;
public errorMessage?: string;
public categories?: ItemCategoryModel[];
private readonly destroyRef = inject(DestroyRef);
constructor(
private fb: FormBuilder,
private httpClient: HttpClient,
private dialogRef: MatDialogRef<SettingsItemEditComponent>,
private itemService: ItemService,
@Inject(MAT_DIALOG_DATA) public item: ItemModel
) {
}
ngOnInit(): void {
this.editForm = this.fb.group({
id: [
this.item.id,
],
name: [
this.item.name,
[Validators.required],
],
category: [
this.item.categoryId,
[Validators.required],
],
});
this.itemService
.getCategories()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(x => this.categories = x);
}
closeDialog(): void {
this.dialogRef.close();
}
onSubmitClick() {
console.log(this.item);
console.log(this.editForm.value);
if (this.editForm.valid) {
this.errorMessage = undefined;
this.httpClient
.put<ItemModel>(ApiRoutes.SettingsItem.replace(':id', this.item.id!), this.editForm.value)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(response => {
this.dialogRef.close({ motion: response, refresh: true });
}, error => {
this.errorMessage = error.detail;
});
}
}
}
@@ -0,0 +1,49 @@
<section class="content">
<div class="content-block">
<div class="block-header">
<!-- breadcrumb -->
<app-breadcrumb [title]="'Item categories'" [items]="['Home','Settings']" [active_item]="'Motion categories'">
</app-breadcrumb>
</div>
<div class="row clearfix">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<mat-card class="card">
<mat-card-header>
<mat-card-title-group>
<mat-card-title>
</mat-card-title>
<mat-card-subtitle>
</mat-card-subtitle>
</mat-card-title-group>
<div fxFlex></div>
<button class="btn-space " (click)="add()" mat-raised-button color="primary">
Add
</button>
</mat-card-header>
<mat-card-content>
<div class="body table-responsive">
<table class="table">
<tbody>
<tr *ngFor="let item of categories">
<td>{{item.name}}</td>
<td>
<mat-icon *ngIf="item.internal">done</mat-icon>
</td>
<td>
<button class="btn-space" (click)="edit(item)" mat-raised-button color="primary">
Edit
</button>
<button class="btn-space" *ngIf="item.allowDelete" (click)="remove(item)" mat-raised-button color="primary">
Delete
</button>
</td>
</tr>
</tbody>
</table>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
</div>
</section>
@@ -0,0 +1,97 @@
// angular
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
// libs
import Swal from 'sweetalert2';
import { MatDialog } from '@angular/material/dialog';
// app
import { ApiRoutes } from '../../../api-routes';
import { ItemCategoryModel } from '../../../model/item.category.model';
import { SettingsItemCategoryEditComponent } from './edit.item.category.component';
@Component({
templateUrl: './item.category.component.html',
styleUrls: ['./item.category.component.scss'],
})
export class SettingsItemCategoryComponent {
public categories?: ItemCategoryModel[];
private readonly destroyRef = inject(DestroyRef);
constructor(
private httpClient: HttpClient,
private dialogModel: MatDialog,
) {
}
ngOnInit(): void {
this.load();
}
private load() {
this.httpClient
.get<ItemCategoryModel[]>(ApiRoutes.SettingsItemCategories)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
this.categories = data;
});
}
public add() {
this.dialogModel.open(SettingsItemCategoryEditComponent, {
width: '640px',
disableClose: true,
data: { },
}).afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(x => {
if (x && x.refresh) {
this.load();
}
});
}
public edit(category: ItemCategoryModel) {
this.dialogModel.open(SettingsItemCategoryEditComponent, {
width: '640px',
disableClose: true,
data: category,
}).afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(x => {
if (x && x.refresh) {
this.load();
}
});
}
public remove(category: ItemCategoryModel) {
Swal.fire({
title: 'Delete item category ' + category.name,
showCancelButton: true,
confirmButtonText: 'Delete',
showLoaderOnConfirm: true,
preConfirm: (name) => {
return new Promise((resolve, reject) => {
this.httpClient.delete(ApiRoutes.SettingsItemCategory.replace(':id', category.id!))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
resolve(data);
}, error => {
Swal.showValidationMessage(error.detail);
reject();
});
}).catch(x => {
return false;
});
},
allowOutsideClick: () => !Swal.isLoading(),
}).then((result) => {
this.load();
});
}
}
@@ -0,0 +1,77 @@
<section class="content">
<div class="content-block">
<div class="block-header">
<!-- breadcrumb -->
<app-breadcrumb [title]="'Blank'" [items]="['Home','Settings']" [active_item]="'Items'">
</app-breadcrumb>
</div>
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-3 col-lg-3">
<div class="card">
<div class="body">
<div id="mail-nav">
<ul>
<mat-form-field class="example-full-width right-content" *ngIf="selectedCount > 0">
<mat-label>Move to category</mat-label>
<mat-select [(ngModel)]="category" (selectionChange)="onCategoryChange($event)">
<mat-option *ngFor="let category of categories" [value]="category.id">
{{category.name}}
</mat-option>
</mat-select>
</mat-form-field>
</ul>
<ul class="" id="mail-folders">
<li *ngFor="let category of categories" [class.active]="selectedCategory && selectedCategory.id == category.id">
<a href="javascript:;" (click)="selectCategory(category)" title="{{category.name}}">{{category.name}}</a>
</li>
</ul>
<ul *ngIf="categoryAddShow">
<mat-form-field class="example-full-width">
<mat-label>Code</mat-label>
<input matInput [(ngModel)]="categoryAddName">
</mat-form-field>
</ul>
<ul *ngIf="!categoryAddShow">
</ul>
</div>
</div>
</div>
</div>
<div class="col-xs-12 col-sm-12 col-md-9 col-lg-9">
<mat-card class="card">
<mat-card-header>
<mat-card-title-group>
<mat-card-title>
Motions
</mat-card-title>
<mat-card-subtitle>
</mat-card-subtitle>
</mat-card-title-group>
<div fxFlex></div>
</mat-card-header>
<mat-card-content>
<div class="body table-responsive">
<table class="table">
<tbody>
<tr *ngFor="let item of items">
<td>
<mat-checkbox [(ngModel)]="item.selected" (change)="onCheckboxChange(item)">
{{item.model.name}}
</mat-checkbox>
</td>
<td>
<button class="btn-space" (click)="edit(item.model)" mat-raised-button color="primary">
Edit
</button>
</td>
</tr>
</tbody>
</table>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
</div>
</section>
@@ -0,0 +1,11 @@
.card-title {
display: flex;
}
.left-content {
flex: 1;
}
.right-content {
margin-left: auto;
}
@@ -0,0 +1,130 @@
// angular
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
// libs
import Swal from 'sweetalert2';
import { MatDialog } from '@angular/material/dialog';
// app
import { ApiRoutes } from '../../../api-routes';
import { SettingsItemEditComponent } from './edit.item.component';
import { ItemService } from '../../../services/item.service';
import { ISelectableModel } from '../../../core/models/selectable.model';
import { ItemModel } from '../../../model/item.model';
import { ItemCategoryModel } from '../../../model/item.category.model';
@Component({
templateUrl: './item.component.html',
styleUrls: ['./item.component.scss'],
})
export class SettingsItemComponent {
public items?: ISelectableModel<ItemModel>[];
public categories?: ItemCategoryModel[];
public selectedCategory?: ItemCategoryModel;
public selectedCount: number = 0;
public category?: ItemCategoryModel;
public categoryAddShow: boolean = false;
public categoryAddName?: string;
private readonly destroyRef = inject(DestroyRef);
constructor(
private dialogModel: MatDialog,
private httpClient: HttpClient,
private itemService: ItemService,
) {
}
ngOnInit(): void {
this.load();
}
private load(categoryId?: string) {
this.itemService.getCategories()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
this.categories = data;
var selected = data[0];
if (categoryId) {
var categories = this.categories.filter(x => x.id === categoryId);
selected = categories.length === 0
? selected
: categories[0];
}
this.selectCategory(selected);
});
}
public selectCategory(category: ItemCategoryModel) {
this.selectedCategory = category;
this.itemService.getItems(category.id)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
this.items = data.map<ISelectableModel<ItemModel>>(x => { return { model: x, selected: false } });
});
}
public edit(item: ItemModel) {
this.dialogModel.open(SettingsItemEditComponent, {
width: '640px',
disableClose: true,
data: item,
}).afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(x => {
if (x && x.refresh) {
console.log(x.motion.categoryId);
this.load(x.motion.categoryId);
}
});
}
public remove(category: ItemModel) {
Swal.fire({
title: 'Delete item from ' + category.name,
showCancelButton: true,
confirmButtonText: 'Delete',
showLoaderOnConfirm: true,
preConfirm: (name) => {
return new Promise((resolve, reject) => {
this.httpClient.delete(ApiRoutes.SettingsItemCategory.replace(':id', category.id!))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
resolve(data);
}, error => {
Swal.showValidationMessage(error.detail);
reject();
});
}).catch(x => {
return false;
});
},
allowOutsideClick: () => !Swal.isLoading(),
}).then((result) => {
this.load();
});
}
onCheckboxChange(item: ISelectableModel<ItemModel>) {
this.selectedCount += item.selected ? 1 : -1;
}
onCategoryChange(item: any) {
var data = {
category: item.value,
items: this.items!.filter(x => x.selected).map(x => x.model.id)
}
this.httpClient.post(ApiRoutes.SettingsItems, data)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
this.category = undefined;
this.selectedCount = 0;
this.load();
}, error => {
});
}
}
@@ -0,0 +1,129 @@
<section class="content">
<div class="content-block">
<div class="block-header">
<!-- breadcrumb -->
<app-breadcrumb [title]="'Profile'" [items]="['User']" [active_item]="'Profile'">
</app-breadcrumb>
</div>
<div class="row clearfix">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<div class="card">
<div class="header">
<h2><strong>User</strong> Profile</h2>
</div>
<div class="body">
<form class="m-4" [formGroup]="form!" (ngSubmit)="onSubmit()">
<div class="row">
<div class="col-xl-12 col-lg-12 col-md-12 col-sm-12 mb-2">
<mat-form-field class="example-full-width" appearance="fill">
<mat-label>Email</mat-label>
<input matInput formControlName="email" autocomplete="off">
<mat-icon class="material-icons-two-tone color-icon p-3" matSuffix>email</mat-icon>
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-xl-6 col-lg-6 col-md-12 col-sm-12 mb-2">
<mat-form-field class="example-full-width" appearance="fill">
<mat-label>First name</mat-label>
<input matInput formControlName="firstName" autocomplete="off">
<mat-icon class="material-icons-two-tone color-icon p-3" matSuffix>face</mat-icon>
<mat-error *ngIf="form.get('firstName')?.hasError('pattern')">
Only characters or numbers allowed
</mat-error>
</mat-form-field>
</div>
<div class="col-xl-6 col-lg-6 col-md-12 col-sm-12 mb-2">
<mat-form-field class="example-full-width" appearance="fill">
<mat-label>Last name</mat-label>
<input matInput formControlName="lastName" autocomplete="off">
<mat-icon class="material-icons-two-tone color-icon p-3" matSuffix>face</mat-icon>
<mat-error *ngIf="form.get('lastName')?.hasError('pattern')">
Only characters or numbers allowed
</mat-error>
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-xl-12 col-lg-12 col-md-12 col-sm-12 mb-2">
<mat-form-field class="example-full-width" appearance="fill">
<mat-label>Full name</mat-label>
<input matInput formControlName="fullName" autocomplete="off">
<mat-icon class="material-icons-two-tone color-icon p-3" matSuffix>face</mat-icon>
<mat-error *ngIf="form.get('fullName')?.hasError('whitespace')">
Full name is empty
</mat-error>
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-xl-12 col-lg-12 col-md-12 col-sm-12 mb-2">
<mat-form-field class="example-full-width" appearance="fill">
<mat-label>Currency</mat-label>
<mat-select formControlName="currency">
<mat-option *ngFor="let item of currencies" [value]="item.id">
({{item.symbol}}) {{item.name}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="row">
<div class="col-xl-12 col-lg-12 col-md-12 col-sm-12 mb-2">
<button class="btn-space" [disabled]="!form.valid" mat-raised-button color="primary">
Submit
</button>
<button type="button" mat-button>Cancel</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<div class="card">
<div class="header">
<h2>
<strong>Providers</strong>
</h2>
</div>
<div class="body">
<div class="row" *ngFor="let provider of providers">
<div class="col-md-1">
<mat-icon class="material-icons-two-tone color-icon" color="secondary" *ngIf="!provider.isConnected" matSuffix>link</mat-icon>
<mat-icon class="material-icons-two-tone color-icon" color="accent" *ngIf="provider.isConnected && !provider.isEmailConfirmed" title="Email not confirmed" matSuffix>email</mat-icon>
<mat-icon class="material-icons-two-tone color-icon" color="primary" *ngIf="provider.isConnected && provider.isEmailConfirmed" title="Email confirmed" matSuffix>mark_email_read</mat-icon>
</div>
<div class="col-md-4">
{{provider.name}}
</div>
<div class="col-md-2">
<span class="fl-r" *ngIf="!provider.isConnected">
<button color="accent" class="btn-block mw-100-imp"
mat-raised-button
(click)="connect(provider)"
[disabled]="provider.isLoading"
[class.spinner]="provider.isLoading"
type="button">
Connect
</button>
</span>
<span class="fl-r" *ngIf="provider.isConnected">
<button color="warn" class="btn-block mw-100-imp"
mat-raised-button
(click)="disconnect(provider.provider)"
[disabled]="provider.isLoading"
[class.spinner]="provider.isLoading"
type="button">
Disconect
</button>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { UserProfileComponent } from './user-profile.component';
describe('UserProfileComponent',
() => {
let component: UserProfileComponent;
let fixture: ComponentFixture<UserProfileComponent>;
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [UserProfileComponent],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(UserProfileComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create',
() => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,178 @@
// angular
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
// libs
import Swal from 'sweetalert2';
// app
import { ApiRoutes } from '../../api-routes';
import { ExternalLoginConfig } from '../../config.external-login';
import { AuthService } from '../../core/service/auth.service';
@Component({
selector: 'app-blank',
templateUrl: './user-profile.component.html',
styleUrls: ['./user-profile.component.scss'],
})
export class UserProfileComponent {
form: FormGroup;
providers?: ProviderModel[];
profile?: ProfileModel;
currencies?: CurrencyModel[];
private readonly destroyRef = inject(DestroyRef);
constructor(
private fb: FormBuilder,
private httpClient: HttpClient,
private authService: AuthService,
) {
this.form = this.fb.group({
firstName: ['', [Validators.pattern('[a-zA-Z0-9]+')]],
lastName: ['', [Validators.pattern('[a-zA-Z0-9]+')]],
fullName: ['', [Validators.pattern('[a-zA-Z0-9 ]+')]],
email: [{ value: '', disabled: true }, []],
currency: [{ value: '' }, []],
});
this.providers = ExternalLoginConfig
.getConfiguredProviders()
.map(x => <ProviderModel>{
provider: x.provider,
name: x.name,
isConnected: false,
isEmailConfirmed: false
});
}
ngOnInit(): void {
this.load();
}
private updateForm(data: ProfileModel) {
this.form.patchValue(data);
}
private load() {
this.loadProfile();
}
private loadProfile() {
this.httpClient
.get<ProfileModel>(ApiRoutes.UserProfile)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
this.profile = data;
this.loadCurrencies();
this.updateForm(data);
this.providers?.map((provider) => {
var attached = data.providers?.find(x => x.provider === provider.provider);
provider.isConnected = false;
provider.isEmailConfirmed = false;
if (attached) {
provider.isConnected = true;
provider.isEmailConfirmed = attached.isEmailConfirmed;
}
});
});
}
private loadCurrencies() {
this.httpClient
.get<CurrencyModel[]>(ApiRoutes.GeneralCurrencies)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
this.currencies = data;
});
}
onSubmit() {
if (this.form.invalid) {
return;
}
this.httpClient.post<ProfileModel>(ApiRoutes.UserProfile, this.form.value)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
this.updateForm(data);
console.log(this.form);
});
}
connect(provider: ProviderModel) {
provider.error = undefined;
provider.isLoading = true;
if (provider.provider === ExternalLoginConfig.GOOGLE) {
this.authService.attachGoogle()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(r => {
this.load();
provider.isLoading = false;
});
}
if (provider.provider === ExternalLoginConfig.AUTH0) {
this.authService.attachAuth0()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(resp => {
provider.error = '';
if (!resp.success) {
provider.error = resp.data || 'Connect failed';
}
this.load();
provider.isLoading = false;
});
}
}
disconnect(provider: string) {
Swal.fire({
title: 'Are you sure to disconnect ' + provider + ' ?',
text: 'Some providers can be connected only with login!',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Yes, disconnect it!',
}).then((result) => {
if (result.value) {
this.authService.deattach(provider)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(x => {
this.load();
});
}
});
}
}
interface ProviderModel {
provider: string;
name: string;
isConnected: boolean;
isEmailConfirmed: boolean;
error?: string;
isLoading: boolean;
}
interface ProfileModel {
firstName?: string,
lastName?: string,
fullName?: string,
currency?: string,
isEmailConfirmed?: boolean,
providers?: ProviderModel[],
}
interface CurrencyModel {
id?: string,
name?: string,
symbol?: string,
rate?: number,
rateDate?: string,
}