diff --git a/MyOffice.Data.Models/Accounts/Account.cs b/MyOffice.Data.Models/Accounts/Account.cs index 95f6fa5..0375ef9 100644 --- a/MyOffice.Data.Models/Accounts/Account.cs +++ b/MyOffice.Data.Models/Accounts/Account.cs @@ -28,7 +28,7 @@ public class AccountDetailed public class AccountSimple { public Guid Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } = null!; public AccountAccessTypeEnum Type { get; set; } public string? CurrencyName { get; set; } public string? CurrencyShortName { get; set; } @@ -42,7 +42,7 @@ public class AccountSimple public class MotionTotalSimple { public Guid? Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } = null!; public string CurrencyId { get; set; } = null!; public string CurrencyName { get; set; } = null!; public decimal CurrencyRate { get; set; } diff --git a/MyOffice.Data.Models/Accounts/Domain/AccountWithRate.cs b/MyOffice.Data.Models/Accounts/Domain/AccountWithRate.cs new file mode 100644 index 0000000..e5d2504 --- /dev/null +++ b/MyOffice.Data.Models/Accounts/Domain/AccountWithRate.cs @@ -0,0 +1,10 @@ +namespace MyOffice.Data.Models.Accounts.Domain; + +using Currencies; + +public class AccountWithRate +{ + public Account Account { get; set; } = null!; + public Currency? Currency { get; set; } + public CurrencyRate? Rate { get; set; } +} diff --git a/MyOffice.Data.Repositories/Account/AccountRepository.cs b/MyOffice.Data.Repositories/Account/AccountRepository.cs index 92a1e54..3feedab 100644 --- a/MyOffice.Data.Repositories/Account/AccountRepository.cs +++ b/MyOffice.Data.Repositories/Account/AccountRepository.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Models.Accounts; +using MyOffice.Data.Models.Accounts.Domain; using MyOffice.Data.Models.Currencies; using MyOffice.Data.Repositories; @@ -117,42 +118,40 @@ public class AccountRepository : AppRepository, IAccountRepository .ToList(); } - public decimal GetPeriodIncome(Guid userId, DateTime from, DateTime to) + public List GetAccountsWithRate(Guid userId, DateTime date) { - return _context.Accounts - .Where(x => x.AccessRights!.Any(u => u.UserId == userId)) - .Include(x => x.CurrencyGlobal!) - .ThenInclude(x => x.Currencies!) - .ThenInclude(x => x.CurrentRate!) - .Select(x => new { - Currency = x.CurrencyGlobal!.Currencies!.FirstOrDefault(x => x.UserId == userId), - Amount = x.Motions! - .Where(x => !x.Item!.Category!.IsInternal) - .Where(x => x.DateTime >= from && x.DateTime <= to) - .Sum(m => m.AmountPlus) - }) - .ToList() - .Select(x => x.Amount * (x.Currency?.CurrentRate?.Rate * x.Currency?.CurrentRate?.Quantity)) - .Sum() ?? 0; - } + var accounts = _context.Accounts + .Where(x => x.AccessRights!.Any(a => a.UserId == userId)) + .ToList(); - public decimal GetPeriodOutcome(Guid userId, DateTime from, DateTime to) - { - return _context.Accounts - .Where(x => x.AccessRights!.Any(u => u.UserId == userId)) - .Include(x => x.CurrencyGlobal!) - .ThenInclude(x => x.Currencies!) - .ThenInclude(x => x.CurrentRate!) - .Select(x => new { - Currency = x.CurrencyGlobal!.Currencies!.FirstOrDefault(x => x.UserId == userId), - Amount = x.Motions! - .Where(x => !x.Item!.Category!.IsInternal) - .Where(x => x.DateTime >= from && x.DateTime <= to) - .Sum(m => m.AmountMinus) + var currencyIds = accounts + .Select(x => x.CurrencyGlobalId) + .ToList(); + + var rates = _context.CurrencyRates + .Include(x => x.Currency) + .Where(x => x.Currency!.UserId == userId) + .Where(x => x.DateTime <= date) + .Where(x => currencyIds.Contains(x.Currency!.CurrencyGlobalId)) + .GroupBy(x => x.Currency!.CurrencyGlobalId) + .Select(x => x.OrderByDescending(r => r.DateTime).First()) + .ToList(); + + return accounts + .Select(account => + { + var rate = rates.FirstOrDefault(rate => rate.Currency!.CurrencyGlobalId == account.CurrencyGlobalId); + + var result = new AccountWithRate + { + Account = account, + Currency = rate?.Currency, + Rate = rate, + }; + + return result; }) - .ToList() - .Select(x => x.Amount * (x.Currency?.CurrentRate?.Rate * x.Currency?.CurrentRate?.Quantity)) - .Sum() ?? 0; + .ToList(); } public List GetRestAtDate(Guid userId, DateTime date) @@ -198,25 +197,31 @@ public class AccountRepository : AppRepository, IAccountRepository public List GetIncomeByCategories(Guid userId, DateTime from, DateTime to) { + var accounts = GetAccountsWithRate(userId, to); + var accountIds = accounts.Select(x => x.Account.Id); + return _context.Motions .Where(x => x.DateTime >= from && x.DateTime <= to) - .Where(x => x.Item.Category!.UserId == userId) + .Where(x => accountIds.Contains(x.AccountId)) .Where(x => !x.Item.Category!.IsInternal) .GroupBy(x => new { Id = x.Item.CategoryId, Name = x.Item.Category!.Name, + IsUserCategory = x.Item.Category!.UserId == userId, CurrencyId = x.Account.CurrencyGlobalId, CurrencyName = x.Account.CurrencyGlobal!.Name, }) + .ToList() .Select(x => new MotionTotalSimple { - Id = x.Key.Id, + Id = x.Key.IsUserCategory ? x.Key.Id : userId, Name = x.Key.Name, CurrencyId = x.Key.CurrencyId, CurrencyName = x.Key.CurrencyName, Amount = x.Sum(a => a.AmountPlus), - }).ToList(); + }) + .ToList(); } public List GetIncomeByCategory(Guid userId, Guid categoryId, DateTime from, DateTime to) @@ -247,21 +252,26 @@ public class AccountRepository : AppRepository, IAccountRepository public List GetOutcomeByCategories(Guid userId, DateTime from, DateTime to) { + var accounts = GetAccountsWithRate(userId, to); + var accountIds = accounts.Select(x => x.Account.Id); + return _context.Motions .Where(x => x.DateTime >= from && x.DateTime <= to) - .Where(x => x.Item.Category!.UserId == userId) + .Where(x => accountIds.Contains(x.AccountId)) .Where(x => !x.Item.Category!.IsInternal) .GroupBy(x => new { Id = x.Item.CategoryId, Name = x.Item.Category!.Name, + IsUserCategory = x.Item.Category!.UserId == userId, CurrencyId = x.Account.CurrencyGlobalId, CurrencyName = x.Account.CurrencyGlobal!.Name, }) .Select(x => new MotionTotalSimple { - Id = x.Key.Id, - Name = x.Key.Name, + Id = x.Key.IsUserCategory ? x.Key.Id : userId, + Name = x.Key.IsUserCategory ? x.Key.Name : "UnCategorized", + //Name = x.Key.Name, CurrencyId = x.Key.CurrencyId, CurrencyName = x.Key.CurrencyName, Amount = x.Sum(a => a.AmountMinus), @@ -271,11 +281,14 @@ public class AccountRepository : AppRepository, IAccountRepository public List GetOutcomeByCategory(Guid userId, Guid categoryId, DateTime from, DateTime to) { + var accounts = GetAccountsWithRate(userId, to); + var accountIds = accounts.Select(x => x.Account.Id); + return _context.Motions .Where(x => x.DateTime >= from && x.DateTime <= to) - .Where(x => x.Item.CategoryId == categoryId) + .Where(x => accountIds.Contains(x.AccountId)) .Where(x => !x.Item.Category!.IsInternal) - .Where(x => x.Item.Category!.UserId == userId) + .Where(x => x.Item.CategoryId == categoryId) .GroupBy(x => new { Id = x.Item.ItemGlobalId, diff --git a/MyOffice.Data.Repositories/Account/IAccountyRepository.cs b/MyOffice.Data.Repositories/Account/IAccountyRepository.cs index cecba97..bed4853 100644 --- a/MyOffice.Data.Repositories/Account/IAccountyRepository.cs +++ b/MyOffice.Data.Repositories/Account/IAccountyRepository.cs @@ -15,8 +15,6 @@ public interface IAccountRepository bool Remove(Account account); List FindAccounts(Guid userId, string term); - 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); diff --git a/MyOffice.Data.Repositories/Item/IItemGlobalRepository.cs b/MyOffice.Data.Repositories/Item/IItemGlobalRepository.cs index 857014b..6568142 100644 --- a/MyOffice.Data.Repositories/Item/IItemGlobalRepository.cs +++ b/MyOffice.Data.Repositories/Item/IItemGlobalRepository.cs @@ -7,4 +7,5 @@ public interface IItemGlobalRepository ItemGlobal? Get(Guid id); ItemGlobal? GetByName(string name); bool Add(ItemGlobal itemGlobal); + List GetAvailableToUser(Guid userId); } \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Item/IItemRepository.cs b/MyOffice.Data.Repositories/Item/IItemRepository.cs index 00c1ed5..2ffe909 100644 --- a/MyOffice.Data.Repositories/Item/IItemRepository.cs +++ b/MyOffice.Data.Repositories/Item/IItemRepository.cs @@ -6,8 +6,7 @@ public interface IItemRepository { List GetAll(Guid userId); List GetByCategory(Guid userId, Guid categoryId); - - Item? GetByGlobal(Guid userId, Guid globalMotionId); + Item? GetByGlobal(Guid userId, Guid globalItemId); bool Add(Item item); bool Update(Item item); List Find(Guid userId, string term, int limit); diff --git a/MyOffice.Data.Repositories/Item/ItemGlobalRepository.cs b/MyOffice.Data.Repositories/Item/ItemGlobalRepository.cs index 364b1d5..2703d36 100644 --- a/MyOffice.Data.Repositories/Item/ItemGlobalRepository.cs +++ b/MyOffice.Data.Repositories/Item/ItemGlobalRepository.cs @@ -1,5 +1,6 @@ namespace MyOffice.Data.Repositories.Item; +using Microsoft.EntityFrameworkCore; using MyOffice.Data.Models.Items; public class ItemGlobalRepository : AppRepository, IItemGlobalRepository @@ -18,4 +19,19 @@ public class ItemGlobalRepository : AppRepository, IItemGlobalReposi { return AddBase(itemGlobal) > 0; } + + public List GetAvailableToUser(Guid userId) + { + return _context.Motions + .Include(x => x.Item) + .ThenInclude(x => x.ItemGlobal) + .ThenInclude(x => x.Items) + .ThenInclude(x => x.Category) + .Where(x => x.Account.AccessRights!.Any(a => a.UserId == userId)) + .Where(x => x.Item.ItemGlobal.Items.All(g => g.Category!.UserId != userId)) + .Select(x => x.Item.ItemGlobal) + .GroupBy(x => x.Id) + .Select(x => x.First()) + .ToList(); + } } \ No newline at end of file diff --git a/MyOffice.Data.Repositories/Item/ItemRepository.cs b/MyOffice.Data.Repositories/Item/ItemRepository.cs index f3a268f..3ce83c9 100644 --- a/MyOffice.Data.Repositories/Item/ItemRepository.cs +++ b/MyOffice.Data.Repositories/Item/ItemRepository.cs @@ -25,13 +25,13 @@ public class ItemRepository : AppRepository, IItemRepository .ToList(); } - public Item? GetByGlobal(Guid userId, Guid globalMotionId) + public Item? GetByGlobal(Guid userId, Guid globalItemId) { return _context.Items .Include(x => x.Motions) .Include(x => x.Category) .Include(x => x.ItemGlobal) - .FirstOrDefault(x => x.Category!.UserId == userId && x.ItemGlobalId == globalMotionId); + .FirstOrDefault(x => x.Category!.UserId == userId && x.ItemGlobalId == globalItemId); } public bool Update(Item item) diff --git a/MyOffice.SPA/src/app/core/models/selectable.model.spec.ts b/MyOffice.SPA/src/app/core/models/selectable.model.spec.ts new file mode 100644 index 0000000..1c4ee7b --- /dev/null +++ b/MyOffice.SPA/src/app/core/models/selectable.model.spec.ts @@ -0,0 +1,7 @@ +import { SelectableModel } from './selectable.model'; + +describe('SelectableModel', () => { + it('should create an instance', () => { + expect(new SelectableModel()).toBeTruthy(); + }); +}); diff --git a/MyOffice.SPA/src/app/core/models/selectable.model.ts b/MyOffice.SPA/src/app/core/models/selectable.model.ts new file mode 100644 index 0000000..bba11c1 --- /dev/null +++ b/MyOffice.SPA/src/app/core/models/selectable.model.ts @@ -0,0 +1,4 @@ +export interface ISelectableModel { + model: T; + selected: boolean; +} diff --git a/MyOffice.SPA/src/app/pages/accounts/account.component.scss b/MyOffice.SPA/src/app/pages/accounts/account.component.scss index d30fa0b..493077c 100644 --- a/MyOffice.SPA/src/app/pages/accounts/account.component.scss +++ b/MyOffice.SPA/src/app/pages/accounts/account.component.scss @@ -1,7 +1,15 @@ -account-motion:nth-child(even) { - filter: brightness(1) +:host-context(body.dark)account-motion:nth-child(even) { + filter: brightness(1); } -account-motion:nth-child(odd) { - filter: brightness(1.75) +: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); } diff --git a/MyOffice.SPA/src/app/pages/accounts/motion.component.ts b/MyOffice.SPA/src/app/pages/accounts/motion.component.ts index 3850fee..43a90cf 100644 --- a/MyOffice.SPA/src/app/pages/accounts/motion.component.ts +++ b/MyOffice.SPA/src/app/pages/accounts/motion.component.ts @@ -129,6 +129,7 @@ export class MotionComponent { 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, diff --git a/MyOffice.SPA/src/app/pages/settings/item/item.component.html b/MyOffice.SPA/src/app/pages/settings/item/item.component.html index 3de48a8..d41045d 100644 --- a/MyOffice.SPA/src/app/pages/settings/item/item.component.html +++ b/MyOffice.SPA/src/app/pages/settings/item/item.component.html @@ -10,6 +10,16 @@
+
    + + Move to category + + + {{category.name}} + + + +
  • {{category.name}} @@ -36,9 +46,13 @@ - + diff --git a/MyOffice.SPA/src/app/pages/settings/item/item.component.scss b/MyOffice.SPA/src/app/pages/settings/item/item.component.scss index e69de29..72edd68 100644 --- a/MyOffice.SPA/src/app/pages/settings/item/item.component.scss +++ b/MyOffice.SPA/src/app/pages/settings/item/item.component.scss @@ -0,0 +1,11 @@ +.card-title { + display: flex; +} + +.left-content { + flex: 1; +} + +.right-content { + margin-left: auto; +} diff --git a/MyOffice.SPA/src/app/pages/settings/item/item.component.ts b/MyOffice.SPA/src/app/pages/settings/item/item.component.ts index 62e7db5..0a441a1 100644 --- a/MyOffice.SPA/src/app/pages/settings/item/item.component.ts +++ b/MyOffice.SPA/src/app/pages/settings/item/item.component.ts @@ -10,6 +10,7 @@ import { MatDialog } from '@angular/material/dialog'; 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'; @@ -18,9 +19,11 @@ import { ItemCategoryModel } from '../../../model/item.category.model'; styleUrls: ['./item.component.scss'], }) export class SettingsItemComponent { - public items?: ItemModel[]; + public items?: ISelectableModel[]; public categories?: ItemCategoryModel[]; public selectedCategory?: ItemCategoryModel; + public selectedCount: number = 0; + public category?: ItemCategoryModel; constructor( private dialogModel: MatDialog, @@ -53,7 +56,7 @@ export class SettingsItemComponent { this.selectedCategory = category; this.itemService.getItems(category.id) .subscribe(data => { - this.items = data; + this.items = data.map>(x => { return { model: x, selected: false } }); }); } @@ -95,4 +98,22 @@ export class SettingsItemComponent { this.load(); }); } + + onCheckboxChange(item: ISelectableModel) { + 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) + .subscribe(data => { + this.category = undefined; + this.selectedCount = 0; + this.load(); + }, error => { + }); + } } diff --git a/MyOffice.Services/Item/ItemService.cs b/MyOffice.Services/Item/ItemService.cs index 7e03ef9..bfb283f 100644 --- a/MyOffice.Services/Item/ItemService.cs +++ b/MyOffice.Services/Item/ItemService.cs @@ -130,6 +130,17 @@ public class ItemService return _mapper.Map>(_itemRepository.GetByCategory(userId, categoryId)); } + public List GetByUncategorized(Guid userId) + { + var itemGlobals = _itemGlobalRepository.GetAvailableToUser(userId); + foreach (var itemGlobal in itemGlobals) + { + GetOrCreate(userId, itemGlobal); + } + + return GetByCategory(userId, userId); + } + public Exec Update(Guid userId, Guid motionId, Guid categoryId) { var result = new Exec(GeneralExecStatus.success); @@ -204,4 +215,25 @@ public class ItemService return _mapper.Map>(_itemRepository.Find(userId, term, limit)); } + + public GeneralExecStatus UpdateItemsCategory(Guid userId, Guid categoryId, List items) + { + var category = _itemCategoryRepository.Get(userId, categoryId); + if (category == null) + { + return GeneralExecStatus.not_found; + } + + foreach (var item in items) + { + var dbItem = _itemRepository.GetByGlobal(userId, item); + if (dbItem != null) + { + dbItem.CategoryId = category.Id; + _itemRepository.Update(dbItem); + } + } + + return GeneralExecStatus.success; + } } diff --git a/MyOffice.Web/Controllers/SettingsItemController.cs b/MyOffice.Web/Controllers/SettingsItemController.cs index 49091cb..40ab898 100644 --- a/MyOffice.Web/Controllers/SettingsItemController.cs +++ b/MyOffice.Web/Controllers/SettingsItemController.cs @@ -101,11 +101,13 @@ public class SettingsItemController : BaseApiController } [HttpGet("~/api/settings/items")] - public ObjectResult Items([AsGuid] string? category) + public ObjectResult Items([AsGuid] string category) { - var list = category.IsMissing() - ? _itemService.GetAll(UserId) - : _itemService.GetByCategory(UserId, category!.AsGuid()); + var categoryId = category.AsGuid(); + + var list = categoryId != UserId + ? _itemService.GetByCategory(UserId, categoryId) + : _itemService.GetByUncategorized(UserId); return Ok(_mapper.Map>(list.OrderBy(x => x.Name))); } @@ -128,4 +130,23 @@ public class SettingsItemController : BaseApiController throw new NotSupportedException(exec.Status.ToString()); } } + + [HttpPost("~/api/settings/items")] + public ObjectResult Items(ItemChangeCategoryModel request) + { + var exec = _itemService.UpdateItemsCategory(UserId, request.category.AsGuid(), request.Items.Select(x => x.AsGuid()).ToList()); + + switch (exec) + { + case GeneralExecStatus.not_found: + return ProblemBadResponse("Category not found."); + case GeneralExecStatus.failure: + return ProblemBadResponse("Update failed."); + case GeneralExecStatus.success: + return Ok(new {}); + + default: + throw new NotSupportedException(exec.ToString()); + } + } } \ No newline at end of file diff --git a/MyOffice.Web/Models/Item/ItemChangeCategoryModel.cs b/MyOffice.Web/Models/Item/ItemChangeCategoryModel.cs new file mode 100644 index 0000000..104544d --- /dev/null +++ b/MyOffice.Web/Models/Item/ItemChangeCategoryModel.cs @@ -0,0 +1,7 @@ +namespace MyOffice.Web.Models.Item; + +public class ItemChangeCategoryModel +{ + public string category { get; set; } = null!; + public List Items { get; set; } = null!; +} \ No newline at end of file diff --git a/MyOffice.Web/Models/Item/ItemEditModel.cs b/MyOffice.Web/Models/Item/ItemEditModel.cs index 314f211..43f161a 100644 --- a/MyOffice.Web/Models/Item/ItemEditModel.cs +++ b/MyOffice.Web/Models/Item/ItemEditModel.cs @@ -1,8 +1,7 @@ -namespace MyOffice.Web.Models.Item +namespace MyOffice.Web.Models.Item; + +public class ItemEditModel { - public class ItemEditModel - { - public string Id { get; set; } = null!; - public string Category { get; set; } = null!; - } -} + public string Id { get; set; } = null!; + public string Category { get; set; } = null!; +} \ No newline at end of file
    {{item.name}} - +