From ad31d3ffcec685a23fb5c80b8e15676116f6d499 Mon Sep 17 00:00:00 2001 From: Alexandr Sulimov Date: Thu, 3 Aug 2023 08:58:53 +0300 Subject: [PATCH] fix --- MyOffice.Core/Extensions/GuidExtensions.cs | 11 +- MyOffice.Core/Extensions/StringExtensions.cs | 2 +- MyOffice.Data.Models/Accounts/Account.cs | 12 +- .../Account/AccountRepository.cs | 62 ++++++++ .../Account/IAccountyRepository.cs | 4 + MyOffice.SPA/MyOffice.SPA.esproj | 2 +- MyOffice.SPA/src/app/api-routes.ts | 2 + .../forgot-password.component.ts | 2 +- .../authentication/signin/signin.component.ts | 2 +- .../authentication/signup/signup.component.ts | 2 +- .../src/app/core/service/auth.service.ts | 2 +- .../app/dashboard/dashboard-routing.module.ts | 15 +- .../src/app/dashboard/dashboard.module.ts | 10 +- .../dashboard/income/dashboard.component.html | 85 ++++++++++ .../dashboard/income/dashboard.component.scss | 0 .../income/dashboard.component.spec.ts | 27 ++++ .../dashboard/income/dashboard.component.ts | 104 ++++++++++++ .../outcome/dashboard.component.html | 80 ++++++++++ .../outcome/dashboard.component.scss | 0 .../outcome/dashboard.component.spec.ts | 27 ++++ .../dashboard/outcome/dashboard.component.ts | 148 ++++++++++++++++++ .../src/app/layout/sidebar/sidebar-items.ts | 24 ++- MyOffice.SPA/src/app/model/dashboard.model.ts | 10 ++ MyOffice.SPA/src/app/pages/pages.module.ts | 2 +- MyOffice.SPA/src/assets/i18n/en.json | 4 +- MyOffice.SPA/src/assets/i18n/ua.json | 8 +- .../Dashboard/DashboardService.cs | 39 ++++- .../Dashboard/Domain/DashboardData.cs | 12 ++ .../Controllers/DashboardController.cs | 21 ++- MyOffice.Web/Program.cs | 2 + 30 files changed, 697 insertions(+), 24 deletions(-) create mode 100644 MyOffice.SPA/src/app/dashboard/income/dashboard.component.html create mode 100644 MyOffice.SPA/src/app/dashboard/income/dashboard.component.scss create mode 100644 MyOffice.SPA/src/app/dashboard/income/dashboard.component.spec.ts create mode 100644 MyOffice.SPA/src/app/dashboard/income/dashboard.component.ts create mode 100644 MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.html create mode 100644 MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.scss create mode 100644 MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.spec.ts create mode 100644 MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.ts diff --git a/MyOffice.Core/Extensions/GuidExtensions.cs b/MyOffice.Core/Extensions/GuidExtensions.cs index 712799c..6a95e4a 100644 --- a/MyOffice.Core/Extensions/GuidExtensions.cs +++ b/MyOffice.Core/Extensions/GuidExtensions.cs @@ -9,12 +9,11 @@ public static class GuidExtensions public static string ToShort(this Guid guid) { - /*string encoded = Convert.ToBase64String(guid.ToByteArray()); - encoded = encoded - .Replace("/", "_") - .Replace("+", "-"); - - return encoded.Substring(0, 22);*/ return guid.ToString("N"); } + + public static string? ToShort(this Guid? guid) + { + return guid?.ToString("N"); + } } \ No newline at end of file diff --git a/MyOffice.Core/Extensions/StringExtensions.cs b/MyOffice.Core/Extensions/StringExtensions.cs index 18e13ce..eca1473 100644 --- a/MyOffice.Core/Extensions/StringExtensions.cs +++ b/MyOffice.Core/Extensions/StringExtensions.cs @@ -54,7 +54,7 @@ public static class StringExtensions return Guid.Parse(str); } - public static Guid? AsGuidNull(this string str) + public static Guid? AsGuidNull(this string? str) { if (Guid.TryParse(str, out var guid)) { diff --git a/MyOffice.Data.Models/Accounts/Account.cs b/MyOffice.Data.Models/Accounts/Account.cs index 92751ef..6ed7ab0 100644 --- a/MyOffice.Data.Models/Accounts/Account.cs +++ b/MyOffice.Data.Models/Accounts/Account.cs @@ -25,7 +25,7 @@ public class AccountDetailed public decimal Rest => TotalPlus - TotalMinus; } -public struct AccountSimple +public class AccountSimple { public Guid Id { get; set; } public string Name { get; set; } @@ -38,3 +38,13 @@ public struct AccountSimple public decimal? TotalMinus { get; set; } public decimal? Balance { get; set; } } + +public class MotionTotalSimple +{ + public Guid? Id { get; set; } + public string Name { get; set; } + public string CurrencyId { get; set; } = null!; + public string CurrencyName { get; set; } = null!; + public decimal CurrencyRate { get; set; } + public decimal Amount { get; set; } +} \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Account/AccountRepository.cs b/MyOffice.Data.Repositories/Account/AccountRepository.cs index 1c65fb0..70f7ae6 100644 --- a/MyOffice.Data.Repositories/Account/AccountRepository.cs +++ b/MyOffice.Data.Repositories/Account/AccountRepository.cs @@ -194,4 +194,66 @@ public class AccountRepository : AppRepository, IAccountRepository }) .ToList(); } + + public List GetIncomeByCategories(Guid userId, DateTime from, DateTime to) + { + return _context.Motions + .Where(x => x.DateTime >= from && x.DateTime <= to) + .Where(x => x.Item.Category!.UserId == userId) + .Where(x => !x.Item.Category!.IsInternal) + .GroupBy(x => new { x.Item.CategoryId, x.Item.Category!.Name }) + .Select(x => new MotionTotalSimple + { + Id = x.Key.CategoryId, + Name = x.Key.Name, + Amount = x.Sum(a => a.AmountPlus) + }).ToList(); + } + + public List GetIncomeByCategory(Guid userId, Guid categoryId, DateTime from, DateTime to) + { + return _context.Motions + .Where(x => x.DateTime >= from && x.DateTime <= to) + .Where(x => x.Item.CategoryId == categoryId) + .Where(x => !x.Item.Category!.IsInternal) + .Where(x => x.Item.Category!.UserId == userId) + .GroupBy(x => new { Id = x.Item.ItemGlobalId, x.Item.ItemGlobal.Name }) + .Select(x => new MotionTotalSimple + { + Name = x.Key.Name, + Amount = x.Sum(a => a.AmountPlus), + }).ToList(); + } + + + public List GetOutcomeByCategories(Guid userId, DateTime from, DateTime to) + { + return _context.Motions + .Where(x => x.DateTime >= from && x.DateTime <= to) + .Where(x => x.Item.Category!.UserId == userId) + .Where(x => !x.Item.Category!.IsInternal) + .GroupBy(x => new { x.Item.CategoryId, x.Item.Category!.Name }) + .Select(x => new MotionTotalSimple + { + Id = x.Key.CategoryId, + Name = x.Key.Name, + Amount = x.Sum(a => a.AmountMinus) + }).ToList(); + } + + + public List GetOutcomeByCategory(Guid userId, Guid categoryId, DateTime from, DateTime to) + { + return _context.Motions + .Where(x => x.DateTime >= from && x.DateTime <= to) + .Where(x => x.Item.CategoryId == categoryId) + .Where(x => !x.Item.Category!.IsInternal) + .Where(x => x.Item.Category!.UserId == userId) + .GroupBy(x => new { Id = x.Item.ItemGlobalId, x.Item.ItemGlobal.Name }) + .Select(x => new MotionTotalSimple + { + Name = x.Key.Name, + Amount = x.Sum(a => a.AmountMinus) + }).ToList(); + } } \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Account/IAccountyRepository.cs b/MyOffice.Data.Repositories/Account/IAccountyRepository.cs index 1c8cf8c..cecba97 100644 --- a/MyOffice.Data.Repositories/Account/IAccountyRepository.cs +++ b/MyOffice.Data.Repositories/Account/IAccountyRepository.cs @@ -18,4 +18,8 @@ public interface IAccountRepository decimal GetPeriodIncome(Guid userId, DateTime from, DateTime to); decimal GetPeriodOutcome(Guid userId, DateTime from, DateTime to); List GetRestAtDate(Guid userId, DateTime date); + List GetIncomeByCategories(Guid userId, DateTime from, DateTime to); + List GetIncomeByCategory(Guid userId, Guid categoryId, DateTime from, DateTime to); + List GetOutcomeByCategories(Guid userId, DateTime from, DateTime to); + List GetOutcomeByCategory(Guid userId, Guid categoryId, DateTime from, DateTime to); } \ No newline at end of file diff --git a/MyOffice.SPA/MyOffice.SPA.esproj b/MyOffice.SPA/MyOffice.SPA.esproj index 85eac4d..590b628 100644 --- a/MyOffice.SPA/MyOffice.SPA.esproj +++ b/MyOffice.SPA/MyOffice.SPA.esproj @@ -8,7 +8,7 @@ - + diff --git a/MyOffice.SPA/src/app/api-routes.ts b/MyOffice.SPA/src/app/api-routes.ts index 0a2c41a..aa93c42 100644 --- a/MyOffice.SPA/src/app/api-routes.ts +++ b/MyOffice.SPA/src/app/api-routes.ts @@ -37,4 +37,6 @@ export class ApiRoutes { static Items = '/api/items'; static Dashboard = '/api/dashboard'; + static DashboardIncome = '/api/dashboard/income'; + static DashboardOutcome = '/api/dashboard/outcome'; } diff --git a/MyOffice.SPA/src/app/authentication/forgot-password/forgot-password.component.ts b/MyOffice.SPA/src/app/authentication/forgot-password/forgot-password.component.ts index a81fdf4..1508e99 100644 --- a/MyOffice.SPA/src/app/authentication/forgot-password/forgot-password.component.ts +++ b/MyOffice.SPA/src/app/authentication/forgot-password/forgot-password.component.ts @@ -44,7 +44,7 @@ export class ForgotPasswordComponent implements OnInit { if (this.authForm.invalid) { return; } else { - this.router.navigate(['/dashboard/main']); + this.router.navigate(['/dashboard']); } } } diff --git a/MyOffice.SPA/src/app/authentication/signin/signin.component.ts b/MyOffice.SPA/src/app/authentication/signin/signin.component.ts index f3a7834..6283251 100644 --- a/MyOffice.SPA/src/app/authentication/signin/signin.component.ts +++ b/MyOffice.SPA/src/app/authentication/signin/signin.component.ts @@ -51,7 +51,7 @@ implements OnInit { this.authService.isAuthenticated$.subscribe(isAuthenticated => { if (isAuthenticated) { - this.router.navigate([this.getRedirect() || '/dashboard/dashboard']); + this.router.navigate([this.getRedirect() || '/dashboard']); } }); } diff --git a/MyOffice.SPA/src/app/authentication/signup/signup.component.ts b/MyOffice.SPA/src/app/authentication/signup/signup.component.ts index 1a1de41..6789299 100644 --- a/MyOffice.SPA/src/app/authentication/signup/signup.component.ts +++ b/MyOffice.SPA/src/app/authentication/signup/signup.component.ts @@ -46,7 +46,7 @@ export class SignupComponent implements OnInit { if (this.authForm.invalid) { return; } - //this.router.navigate(['/admin/dashboard/main']); + this.authService.register({ username: this.authForm.get('email')!.value!, password: this.authForm.get('password')!.value!, diff --git a/MyOffice.SPA/src/app/core/service/auth.service.ts b/MyOffice.SPA/src/app/core/service/auth.service.ts index f401d09..d09d1e5 100644 --- a/MyOffice.SPA/src/app/core/service/auth.service.ts +++ b/MyOffice.SPA/src/app/core/service/auth.service.ts @@ -85,7 +85,7 @@ export class AuthService { this.setCurrentUserValue(userProfile.id, userProfile); if (authState.action === AuthenticatedActionEnum.loggedIn) { - this.router.navigate([redirectUrl || '/dashboard/dashboard']); + this.router.navigate([redirectUrl || '/dashboard']); } }); } diff --git a/MyOffice.SPA/src/app/dashboard/dashboard-routing.module.ts b/MyOffice.SPA/src/app/dashboard/dashboard-routing.module.ts index f7825be..3480de6 100644 --- a/MyOffice.SPA/src/app/dashboard/dashboard-routing.module.ts +++ b/MyOffice.SPA/src/app/dashboard/dashboard-routing.module.ts @@ -1,7 +1,12 @@ +// angular import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; + +// app import { Page404Component } from '../authentication/page404/page404.component'; import { Dashboard2Component } from './dashboard2/dashboard2.component'; +import { DashboardIncomeComponent } from './income/dashboard.component'; +import { DashboardOutcomeComponent } from './outcome/dashboard.component'; const routes: Routes = [ { @@ -10,9 +15,17 @@ const routes: Routes = [ pathMatch: 'full', }, { - path: 'dashboard', + path: 'rests', component: Dashboard2Component, }, + { + path: 'income', + component: DashboardIncomeComponent, + }, + { + path: 'outcome', + component: DashboardOutcomeComponent, + }, { path: '**', component: Page404Component }, ]; diff --git a/MyOffice.SPA/src/app/dashboard/dashboard.module.ts b/MyOffice.SPA/src/app/dashboard/dashboard.module.ts index 38dcc16..46d3b05 100644 --- a/MyOffice.SPA/src/app/dashboard/dashboard.module.ts +++ b/MyOffice.SPA/src/app/dashboard/dashboard.module.ts @@ -15,15 +15,22 @@ import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatTooltipModule } from '@angular/material/tooltip'; import { NgApexchartsModule } from 'ng-apexcharts'; import { TranslateModule } from '@ngx-translate/core'; +import { MAT_DATE_LOCALE } from '@angular/material/core'; // app import { DashboardRoutingModule } from './dashboard-routing.module'; import { Dashboard2Component } from './dashboard2/dashboard2.component'; +import { DashboardIncomeComponent } from './income/dashboard.component'; +import { DashboardOutcomeComponent } from './outcome/dashboard.component'; import { ComponentsModule } from 'src/app/shared/components/components.module'; import { SharedModule } from '../shared/shared.module'; @NgModule({ - declarations: [Dashboard2Component], + declarations: [ + Dashboard2Component, + DashboardIncomeComponent, + DashboardOutcomeComponent, + ], imports: [ CommonModule, DashboardRoutingModule, @@ -42,6 +49,7 @@ import { SharedModule } from '../shared/shared.module'; TranslateModule, ], providers: [ + { provide: MAT_DATE_LOCALE, useValue: 'en-GB' }, DecimalPipe, ] }) diff --git a/MyOffice.SPA/src/app/dashboard/income/dashboard.component.html b/MyOffice.SPA/src/app/dashboard/income/dashboard.component.html new file mode 100644 index 0000000..5792652 --- /dev/null +++ b/MyOffice.SPA/src/app/dashboard/income/dashboard.component.html @@ -0,0 +1,85 @@ +
+
+
+ + +
+ + + +
+ +
+
+
+

{{'INCOME' | translate}}

+ + + + + + +
+
+
+ + +
+
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/dashboard/income/dashboard.component.scss b/MyOffice.SPA/src/app/dashboard/income/dashboard.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/dashboard/income/dashboard.component.spec.ts b/MyOffice.SPA/src/app/dashboard/income/dashboard.component.spec.ts new file mode 100644 index 0000000..00dd7f9 --- /dev/null +++ b/MyOffice.SPA/src/app/dashboard/income/dashboard.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Dashboard2Component } from './dashboard2.component'; + +describe('Dashboard2Component', + () => { + let component: Dashboard2Component; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [Dashboard2Component] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(Dashboard2Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', + () => { + expect(component).toBeTruthy(); + }); + }); diff --git a/MyOffice.SPA/src/app/dashboard/income/dashboard.component.ts b/MyOffice.SPA/src/app/dashboard/income/dashboard.component.ts new file mode 100644 index 0000000..309c6fc --- /dev/null +++ b/MyOffice.SPA/src/app/dashboard/income/dashboard.component.ts @@ -0,0 +1,104 @@ +// angular +import { Component } from '@angular/core'; +import { OnInit } from '@angular/core'; +import { ViewChild } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { HttpParams } from '@angular/common/http'; +import { DecimalPipe } from '@angular/common'; + +// libs +import { + ChartComponent, + ApexAxisChartSeries, + ApexTitleSubtitle, + ApexDataLabels, + ApexChart, + ApexPlotOptions, + ApexLegend +} from "ng-apexcharts"; + +// app +import { ApiRoutes } from '../../api-routes'; +import { DashboardInOutModel } from '../../model/dashboard.model'; + +export type ChartOptions = { + series: ApexAxisChartSeries; + chart: ApexChart; + dataLabels: ApexDataLabels; + title: ApexTitleSubtitle; + plotOptions: ApexPlotOptions; + legend: ApexLegend; +}; + +@Component({ + selector: 'app-dashboard-income', + templateUrl: './dashboard.component.html', + styleUrls: ['./dashboard.component.scss'], +}) +export class DashboardIncomeComponent implements OnInit { + + @ViewChild("chart") chart!: ChartComponent; + public chartOptions!: Partial; + dashboardModel?: DashboardInOutModel; + + colors: string[] = ["#fd7f6f", "#7eb0d5", "#b2e061", "#bd7ebe", "#ffb55a", "#ffee65", "#beb9db", "#fdcce5", "#8bd3c7"]; + + constructor( + private httpClient: HttpClient, + private _decimalPipe: DecimalPipe + ) { + + } + + ngOnInit() { + let params = new HttpParams() + .set('from', '2023-07-01') + .set('to', '2023-08-01'); + + this.httpClient + .get(ApiRoutes.DashboardIncome, { params: params }) + .subscribe(response => { + var series = response.data.map(x => ({ + x: x.name + '(' + this._decimalPipe.transform(x.value, '1.2-2') || "" + ')', + y: x.value + })); + var min = Math.min(...response.data.map(x => x.value)); + var max = Math.max(...response.data.map(x => x.value)); + var step = max / 10; + var ranges = []; + for (var i = 0; i < this.colors.length; i++) { + ranges.push({ + from: i === 0 ? min : step * i, + to: i === this.colors.length - 1 ? max + 10 : step * (i + 1), + color: this.colors[this.colors.length - i - 1], + }); + } + this.setChartOptions(series, ranges); + }); + } + + private setChartOptions(data: any[], ranges: any[]) { + var self = this; + + this.chartOptions = { + series: [{ + data: data + }], + chart: { + type: 'treemap', + width: 600, + height: 600, + }, + plotOptions: { + treemap: { + enableShades: true, + shadeIntensity: 0.5, + reverseNegativeShade: true, + colorScale: { + ranges: ranges + } + } + }, + }; + } +} diff --git a/MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.html b/MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.html new file mode 100644 index 0000000..1fb082e --- /dev/null +++ b/MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.html @@ -0,0 +1,80 @@ +
+
+
+ + +
+ +
+
+
+
+

{{'PERIOD' | translate}}

+
+
+
+
+
+
+ + From + + YYYY/MM/DD + + + + Please enter rate date + + +
+
+ + To + + YYYY/MM/DD + + + + Please enter rate date + + +
+
+
+
+
+
+
+
+
+ +
+
+
+

{{'OUTCOME' | translate}}

+ +
+
+ + +
+
+
+
+
+
diff --git a/MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.scss b/MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.spec.ts b/MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.spec.ts new file mode 100644 index 0000000..00dd7f9 --- /dev/null +++ b/MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Dashboard2Component } from './dashboard2.component'; + +describe('Dashboard2Component', + () => { + let component: Dashboard2Component; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [Dashboard2Component] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(Dashboard2Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', + () => { + expect(component).toBeTruthy(); + }); + }); diff --git a/MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.ts b/MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.ts new file mode 100644 index 0000000..c4c0fb6 --- /dev/null +++ b/MyOffice.SPA/src/app/dashboard/outcome/dashboard.component.ts @@ -0,0 +1,148 @@ +// angular +import { Component } from '@angular/core'; +import { OnInit } from '@angular/core'; +import { ViewChild } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { HttpParams } from '@angular/common/http'; +import { DecimalPipe } from '@angular/common'; +import { UntypedFormBuilder } from '@angular/forms'; +import { UntypedFormGroup } from '@angular/forms'; +import { Validators } from '@angular/forms'; +import { Inject } from '@angular/core'; + +// libs +import { + ChartComponent, + ApexAxisChartSeries, + ApexTitleSubtitle, + ApexDataLabels, + ApexChart, + ApexPlotOptions, + ApexLegend +} from "ng-apexcharts"; +import * as moment from 'moment' +import { MAT_DATE_FORMATS } from '@angular/material/core'; + +// app +import { ApiRoutes } from '../../api-routes'; +import { DashboardInOutModel } from '../../model/dashboard.model'; + +export type ChartOptions = { + series: ApexAxisChartSeries; + chart: ApexChart; + dataLabels: ApexDataLabels; + title: ApexTitleSubtitle; + plotOptions: ApexPlotOptions; + legend: ApexLegend; +}; + +@Component({ + selector: 'app-dashboard-outcome', + templateUrl: './dashboard.component.html', + styleUrls: ['./dashboard.component.scss'], +}) +export class DashboardOutcomeComponent implements OnInit { + + @ViewChild("chart") chart!: ChartComponent; + public chartOptions!: Partial; + dashboardModel?: DashboardInOutModel; + public form!: UntypedFormGroup; + public dateFrom: Date = moment(new Date).add(-30, 'days').toDate(); + public dateTo: Date = moment(new Date).add(0, 'days').toDate(); + + colors: string[] = ["#fd7f6f", "#7eb0d5", "#b2e061", "#bd7ebe", "#ffb55a", "#ffee65", "#beb9db", "#fdcce5", "#8bd3c7"]; + + constructor( + private fb: UntypedFormBuilder, + private httpClient: HttpClient, + private _decimalPipe: DecimalPipe + ) { + } + + ngOnInit() { + this.form = this.fb.group({ + dateFrom: [ + this.dateFrom, + [Validators.required], + ], + dateTo: [ + this.dateTo, + [Validators.required], + ], + }); + + this.loadData(); + } + + onSubmitClick() { + + } + + private loadData(update?: boolean, category?: string) { + let params = new HttpParams() + .set('from', '2023-07-01') + .set('to', '2023-08-01') + .set('category', category || '') + ; + + this.httpClient + .get(ApiRoutes.DashboardOutcome, { params: params }) + .subscribe(response => { + this.dashboardModel = response; + + var series = response.data.map(x => ({ + x: x.name + ' (' + (this._decimalPipe.transform(x.value, '1.2-2') || '') + ')', + y: x.value + })); + var min = Math.min(...response.data.map(x => x.value)); + var max = Math.max(...response.data.map(x => x.value)); + var step = max / 10; + var ranges = []; + for (var i = 0; i < this.colors.length; i++) { + ranges.push({ + from: i === 0 ? min : step * i, + to: i === this.colors.length - 1 ? max + 10 : step * (i + 1), + color: this.colors[this.colors.length - i - 1], + }); + } + if (update) { + this.chart.updateSeries([{ + data: series + }]); + } else { + this.setChartOptions(series, ranges); + } + }); + } + + private setChartOptions(data: any[], ranges: any[]) { + var self = this; + + this.chartOptions = { + series: [{ + data: data + }], + chart: { + type: 'treemap', + width: 600, + height: 600, + events: { + click: function (event, chartContext, config) { + var data = self.dashboardModel!.data[config.dataPointIndex]; + self.loadData(true, data.id); + } + } + }, + plotOptions: { + treemap: { + enableShades: true, + shadeIntensity: 0.5, + reverseNegativeShade: true, + colorScale: { + ranges: ranges + } + } + }, + }; + } +} diff --git a/MyOffice.SPA/src/app/layout/sidebar/sidebar-items.ts b/MyOffice.SPA/src/app/layout/sidebar/sidebar-items.ts index f74bfb0..08d9e58 100644 --- a/MyOffice.SPA/src/app/layout/sidebar/sidebar-items.ts +++ b/MyOffice.SPA/src/app/layout/sidebar/sidebar-items.ts @@ -23,7 +23,7 @@ export const ROUTES: RouteInfo[] = [ badgeClass: '', submenu: [ { - path: 'dashboard/dashboard', + path: 'dashboard/rests', title: 'MENUITEMS.DASHBOARD.LIST.DASHBOARD', iconType: '', icon: '', @@ -33,6 +33,28 @@ export const ROUTES: RouteInfo[] = [ badgeClass: '', submenu: [], }, + { + path: 'dashboard/income', + title: 'MENUITEMS.DASHBOARD.LIST.INCOME', + iconType: '', + icon: '', + class: 'ml-menu', + groupTitle: false, + badge: '', + badgeClass: '', + submenu: [], + }, + { + path: 'dashboard/outcome', + title: 'MENUITEMS.DASHBOARD.LIST.OUTCOME', + iconType: '', + icon: '', + class: 'ml-menu', + groupTitle: false, + badge: '', + badgeClass: '', + submenu: [], + }, ], }, diff --git a/MyOffice.SPA/src/app/model/dashboard.model.ts b/MyOffice.SPA/src/app/model/dashboard.model.ts index bb73130..33acedd 100644 --- a/MyOffice.SPA/src/app/model/dashboard.model.ts +++ b/MyOffice.SPA/src/app/model/dashboard.model.ts @@ -23,3 +23,13 @@ export interface DashboardRestModel { currencyQuantity: number; balanceAtRate: number; } + +export interface DashboardInOutModel { + data: DashboardInOutItemModel[]; +} + +export interface DashboardInOutItemModel { + id: string; + name: string; + value: number; +} diff --git a/MyOffice.SPA/src/app/pages/pages.module.ts b/MyOffice.SPA/src/app/pages/pages.module.ts index 56c9715..79a21f8 100644 --- a/MyOffice.SPA/src/app/pages/pages.module.ts +++ b/MyOffice.SPA/src/app/pages/pages.module.ts @@ -3,7 +3,6 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms'; -import { MAT_DATE_LOCALE } from '@angular/material/core'; // libs import { MatFormFieldModule } from '@angular/material/form-field'; @@ -22,6 +21,7 @@ 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'; diff --git a/MyOffice.SPA/src/assets/i18n/en.json b/MyOffice.SPA/src/assets/i18n/en.json index 7e925d3..2f87228 100644 --- a/MyOffice.SPA/src/assets/i18n/en.json +++ b/MyOffice.SPA/src/assets/i18n/en.json @@ -14,7 +14,9 @@ "DASHBOARD": { "TEXT": "Dashboard", "LIST": { - "DASHBOARD": "Dashboard" + "DASHBOARD": "Rests", + "INCOME": "Incomes", + "OUTCOME": "Outcomes" } }, "ACCOUNTS": { diff --git a/MyOffice.SPA/src/assets/i18n/ua.json b/MyOffice.SPA/src/assets/i18n/ua.json index c7afb8f..b590262 100644 --- a/MyOffice.SPA/src/assets/i18n/ua.json +++ b/MyOffice.SPA/src/assets/i18n/ua.json @@ -14,7 +14,9 @@ "DASHBOARD": { "TEXT": "Панелі", "LIST": { - "DASHBOARD": "Панель" + "DASHBOARD": "Залишки", + "INCOME": "Надходження", + "OUTCOME": "Витрати" } }, "ACCOUNTS": { @@ -99,5 +101,7 @@ "CURRENT.BALANCE": "Поточний баланс", "HOME": "Головна", "OTHER": "Інші", - "TOTAL": "Всього" + "TOTAL": "Всього", + "INCOME": "Надходження", + "OUTCOME": "Витрати" } diff --git a/MyOffice.Services/Dashboard/DashboardService.cs b/MyOffice.Services/Dashboard/DashboardService.cs index 935f999..85aa36c 100644 --- a/MyOffice.Services/Dashboard/DashboardService.cs +++ b/MyOffice.Services/Dashboard/DashboardService.cs @@ -4,6 +4,7 @@ using MyOffice.Services.Dashboard.Domain; using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using Core.Extensions; @@ -34,11 +35,11 @@ public class DashboardService Balance = rests .Where(x => (x.Type == AccountAccessTypeEnum.balance || x.Type == AccountAccessTypeEnum.credit) && x.CurrencyRate.HasValue) .Sum(x => x.Balance * (x.CurrencyRate * x.CurrencyQuantity)), - + BalanceDebit = rests .Where(x => x.Type == AccountAccessTypeEnum.balance && x.CurrencyRate.HasValue) .Sum(x => x.Balance * (x.CurrencyRate * x.CurrencyQuantity)), - + BalanceCredit = rests .Where(x => x.Type == AccountAccessTypeEnum.credit && x.CurrencyRate.HasValue) .Sum(x => x.Balance * (x.CurrencyRate * x.CurrencyQuantity)), @@ -62,4 +63,38 @@ public class DashboardService return result; } + + public DashboardIncomeData GetDashboardIncomeData(Guid userId, DateTime from, DateTime to, Guid? category) + { + var data = category.HasValue + ? _accountRepository.GetIncomeByCategory(userId, category.Value, from.ToUtc(), to.ToUtc()) + : _accountRepository.GetIncomeByCategories(userId, from.ToUtc(), to.ToUtc()); + + return new DashboardIncomeData + { + Data = data.Select(x => new DashboardIncomeDataItem + { + Id = x.Id?.ToShort(), + Name = x.Name, + Value = x.Amount, + }).ToList() + }; + } + + public DashboardIncomeData GetDashboardOutcomeData(Guid userId, DateTime from, DateTime to, Guid? category) + { + var data = category.HasValue + ? _accountRepository.GetOutcomeByCategory(userId, category.Value, from.ToUtc(), to.ToUtc()) + : _accountRepository.GetOutcomeByCategories(userId, from.ToUtc(), to.ToUtc()); + + return new DashboardIncomeData + { + Data = data.Select(x => new DashboardIncomeDataItem + { + Id = x.Id?.ToShort(), + Name = x.Name, + Value = x.Amount, + }).ToList() + }; + } } diff --git a/MyOffice.Services/Dashboard/Domain/DashboardData.cs b/MyOffice.Services/Dashboard/Domain/DashboardData.cs index 1a60644..6b26b79 100644 --- a/MyOffice.Services/Dashboard/Domain/DashboardData.cs +++ b/MyOffice.Services/Dashboard/Domain/DashboardData.cs @@ -27,4 +27,16 @@ public class DashboardRestData public decimal? CurrencyQuantity { get; set; } public decimal? BalanceAtRate => Balance * (CurrencyRate * CurrencyQuantity); +} + +public class DashboardIncomeData +{ + public List Data { get; set; } = null!; +} + +public class DashboardIncomeDataItem +{ + public string? Id { get; set; } = null!; + public string Name { get; set; } = null!; + public decimal Value { get; set; } } \ No newline at end of file diff --git a/MyOffice.Web/Controllers/DashboardController.cs b/MyOffice.Web/Controllers/DashboardController.cs index cc2d315..6e36900 100644 --- a/MyOffice.Web/Controllers/DashboardController.cs +++ b/MyOffice.Web/Controllers/DashboardController.cs @@ -1,9 +1,9 @@ namespace MyOffice.Web.Controllers; +using Core.Extensions; +using Infrastructure.Attributes; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using MyOffice.Services.Account; -using MyOffice.Services.Item; using Services.Dashboard; [Authorize] @@ -31,4 +31,21 @@ public class DashboardController : BaseApiController return data; } + + [HttpGet("~/api/dashboard/income")] + public object Income(DateTime from, DateTime to, [AsGuid(true)] string? category) + { + var data = _dashboardService.GetDashboardIncomeData(UserId, from.StartOfDay(), to.EndOfDay(), category?.AsGuidNull()); + + return data; + } + + + [HttpGet("~/api/dashboard/outcome")] + public object Outcome(DateTime from, DateTime to, [AsGuid(true)] string? category) + { + var data = _dashboardService.GetDashboardOutcomeData(UserId, from.StartOfDay(), to.EndOfDay(), category?.AsGuidNull()); + + return data; + } } diff --git a/MyOffice.Web/Program.cs b/MyOffice.Web/Program.cs index 4dad839..bc52f80 100644 --- a/MyOffice.Web/Program.cs +++ b/MyOffice.Web/Program.cs @@ -69,6 +69,8 @@ using MyOffice.Services.Mapper; //TODO: SPA all http requests -> services //TODO: Items, select category -> save url to allow refresh //TODO: Accounts, select category -> save url to allow refresh +//TODO: report income +//TODO: report outcome public class Program {