diff --git a/README.md b/README.md index f032032..759a797 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ -# domBudg +# Курсовая работа "ДомБюдж" +## Описание +Система "ДомБюдж" предназначена для ведения домашнего бюджета. В системе можно: + - Вести учет трат и пополнений баланса + - Группировать траты + - Создавать планы трат для групп расходов + - Получать отчет по смещению расходов относительно плана + - Получать отчет расходов за указанный промежуток времени -Курсовая работа "ДомБюдж" \ No newline at end of file +## Как запустить +Чтобы запустить, нужно ввести: +```bash + docker compose up -d +``` +Таким образом будет запущена система в фоновом режиме. +Чтобы перейти в веб приложение, нужно перейти на **80 порт** машины, на которой запущена система. \ No newline at end of file diff --git a/back/Contracts/Repositories/ISpendingGroupRepo.cs b/back/Contracts/Repositories/ISpendingGroupRepo.cs index f8f7999..54f4476 100644 --- a/back/Contracts/Repositories/ISpendingGroupRepo.cs +++ b/back/Contracts/Repositories/ISpendingGroupRepo.cs @@ -6,6 +6,7 @@ namespace Contracts.Repositories; public interface ISpendingGroupRepo { Task Get(SpendingGroupSearch search); + Task GetByPlan(SpendingPlanSearch search); Task> GetList(SpendingGroupSearch? search = null); Task Create(SpendingGroupDto spendingGroup); Task Delete(SpendingGroupSearch search); diff --git a/back/Contracts/Services/IReportOffsetFromPlanService.cs b/back/Contracts/Services/IReportOffsetFromPlanService.cs new file mode 100644 index 0000000..f9c51ed --- /dev/null +++ b/back/Contracts/Services/IReportOffsetFromPlanService.cs @@ -0,0 +1,9 @@ +using Contracts.SearchModels; +using Contracts.ViewModels; + +namespace Contracts.Services; + +public interface IReportOffsetFromPlanService +{ + public Task GetReportData(SpendingPlanSearch search); +} \ No newline at end of file diff --git a/back/Contracts/Services/IReportPeriodService.cs b/back/Contracts/Services/IReportPeriodService.cs new file mode 100644 index 0000000..1b3f6d9 --- /dev/null +++ b/back/Contracts/Services/IReportPeriodService.cs @@ -0,0 +1,8 @@ +using Contracts.ViewModels; + +namespace Contracts.Services; + +public interface IReportPeriodService +{ + Task> GetReportData(DateTime from, DateTime to, Guid userId); +} \ No newline at end of file diff --git a/back/Controllers/Controllers/ReportController.cs b/back/Controllers/Controllers/ReportController.cs new file mode 100644 index 0000000..c844aa9 --- /dev/null +++ b/back/Controllers/Controllers/ReportController.cs @@ -0,0 +1,62 @@ +using Contracts.Services; +using Contracts.ViewModels; +using Microsoft.AspNetCore.Mvc; +using Services.Support.Exceptions; + +namespace Controllers.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class ReportController : ControllerBase +{ + private readonly IReportPeriodService _reportPeriodService; + private readonly IReportOffsetFromPlanService _reportOffsetFromPlanService; + + public ReportController(IReportPeriodService reportPeriodService, + IReportOffsetFromPlanService reportOffsetFromPlanService) + { + _reportPeriodService = reportPeriodService; + _reportOffsetFromPlanService = reportOffsetFromPlanService; + } + + [HttpGet("period/{id}")] + public async Task>> GetReportData( + [FromQuery] DateTime from, [FromQuery] DateTime to, Guid id) + { + try + { + var periodData = await _reportPeriodService.GetReportData(from, to, id); + return Ok(periodData); + } + catch (ReportDataNotFoundException ex) + { + return NotFound(ex.Message); + } + catch (Exception ex) + { + return StatusCode(500, ex.Message); + } + } + + [HttpGet("plan/{id}")] + public async Task> GetReportOffsetFromPlan(Guid id) + { + try + { + var offset = await _reportOffsetFromPlanService.GetReportData(new() { Id = id }); + return Ok(offset); + } + catch (ReportDataNotFoundException ex) + { + return NotFound(ex.Message); + } + catch (EntityNotFoundException ex) + { + return NotFound(ex.Message); + } + catch (Exception ex) + { + return StatusCode(500, ex.Message); + } + } +} \ No newline at end of file diff --git a/back/Controllers/Extensions/AddDomainServicesExt.cs b/back/Controllers/Extensions/AddDomainServicesExt.cs index 7d6cc0a..1a7fc59 100644 --- a/back/Controllers/Extensions/AddDomainServicesExt.cs +++ b/back/Controllers/Extensions/AddDomainServicesExt.cs @@ -1,5 +1,6 @@ using Contracts.Services; using Services.Domain; +using Services.Reports; namespace Controllers.Extensions; @@ -9,11 +10,10 @@ public static class AddDomainServicesExtension { services.AddTransient(); services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + + services.AddTransient(); } } \ No newline at end of file diff --git a/back/Controllers/Extensions/AddRepotServicesExt.cs b/back/Controllers/Extensions/AddRepotServicesExt.cs new file mode 100644 index 0000000..6c2832b --- /dev/null +++ b/back/Controllers/Extensions/AddRepotServicesExt.cs @@ -0,0 +1,13 @@ +using Contracts.Services; +using Services.Reports; + +namespace Controllers.Extensions; + +public static class AddReportServicesExtension +{ + public static void AddReportServices(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + } +} \ No newline at end of file diff --git a/back/Controllers/Program.cs b/back/Controllers/Program.cs index 2c70319..de3d080 100644 --- a/back/Controllers/Program.cs +++ b/back/Controllers/Program.cs @@ -6,6 +6,7 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbConnectionService(builder.Configuration); builder.Services.AddRepos(); builder.Services.AddDomainServices(); +builder.Services.AddReportServices(); builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle diff --git a/back/Infrastructure/Repositories/ChangeRecordRepo.cs b/back/Infrastructure/Repositories/ChangeRecordRepo.cs index 289d89e..a823483 100644 --- a/back/Infrastructure/Repositories/ChangeRecordRepo.cs +++ b/back/Infrastructure/Repositories/ChangeRecordRepo.cs @@ -46,6 +46,7 @@ public class ChangeRecordRepo : IChangeRecordRepo using var context = _factory.CreateDbContext(); var record = await context.ChangeRecords + .Include(x => x.SpendingGroup) .FirstOrDefaultAsync(x => x.Id == search.Id); if (record == null) { diff --git a/back/Infrastructure/Repositories/SpendingGroupRepo.cs b/back/Infrastructure/Repositories/SpendingGroupRepo.cs index e35215f..b7a36a3 100644 --- a/back/Infrastructure/Repositories/SpendingGroupRepo.cs +++ b/back/Infrastructure/Repositories/SpendingGroupRepo.cs @@ -46,6 +46,7 @@ public class SpendingGroupRepo : ISpendingGroupRepo using var context = _factory.CreateDbContext(); var group = await context.SpendingGroups + .AsNoTracking() .Include(x => x.ChangeRecords) .Include(x => x.SpendingPlans) .FirstOrDefaultAsync(x => x.Id == search.Id @@ -56,11 +57,43 @@ public class SpendingGroupRepo : ISpendingGroupRepo return group?.ToDto(); } + public async Task GetByPlan(SpendingPlanSearch search) + { + using var context = _factory.CreateDbContext(); + var plan = await context.SpendingPlans + .AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == search.Id); + + if (plan == null) + { + return null; + } + + var group = await context.SpendingGroups + .AsNoTracking() + .Where(x => x.Id == plan.SpendingGroupId) + .Include(x => x.ChangeRecords! + // Выбираем из них только те, которые попадают в диапазон дат плана + .Where(cg => cg.ChangedAt >= plan.StartAt && cg.ChangedAt <= plan.EndAt) + // И сортируем их по дате + .OrderBy(cg => cg.ChangedAt) + ) + .FirstOrDefaultAsync(); + if (group == null) + { + return null; + } + + group.SpendingPlans = [plan]; + + return group.ToDto(); + } + public async Task> GetList(SpendingGroupSearch? search = null) { using var context = _factory.CreateDbContext(); - var query = context.SpendingGroups.AsQueryable(); + var query = context.SpendingGroups.AsNoTracking().AsQueryable(); if (search != null) { @@ -69,10 +102,13 @@ public class SpendingGroupRepo : ISpendingGroupRepo query = query.Where(x => x.Id == search.Id); } - if (!string.IsNullOrWhiteSpace(search.Name) && search.UserId.HasValue) + if (search.UserId.HasValue) { - query = query.Where(x => x.Name.Contains(search.Name, StringComparison.OrdinalIgnoreCase) - && x.UserId == search.UserId); + query = query.Where(x => x.UserId == search.UserId); + if (!string.IsNullOrWhiteSpace(search.Name)) + { + query = query.Where(x => x.Name.Contains(search.Name, StringComparison.OrdinalIgnoreCase)); + } } } diff --git a/back/Services.Tests/Reports/ReportOffsetServiceTests.cs b/back/Services.Tests/Reports/ReportOffsetServiceTests.cs new file mode 100644 index 0000000..74824d8 --- /dev/null +++ b/back/Services.Tests/Reports/ReportOffsetServiceTests.cs @@ -0,0 +1,56 @@ +using Contracts.DTO; +using Contracts.Repositories; +using Contracts.SearchModels; +using Contracts.ViewModels; +using Moq; +using Services.Reports; +using Services.Support.Exceptions; + +namespace Services.Tests.Reports; + +public class ReportOffsetServiceTests +{ + [Fact] + public void GetReportData_WhenSpendingGroupNotFound_ThenThrowsEntityNotFoundException() + { + var spendingGroupRepoMock = new Mock(); + spendingGroupRepoMock.Setup(repo => repo.GetByPlan(It.IsAny())).ReturnsAsync((SpendingGroupDto)null); + var reportOffsetService = new ReportOffsetFromPlanService(spendingGroupRepoMock.Object); + + Assert.ThrowsAsync(() => reportOffsetService.GetReportData(new())); + } + + [Fact] + public void GetReportData_WhenSpendingGroupHasNoChangeRecords_ThenThrowsReportDataNotFoundException() + { + var spendingGroupRepoMock = new Mock(); + spendingGroupRepoMock.Setup(repo => repo.GetByPlan(It.IsAny())).ReturnsAsync(new SpendingGroupDto()); + var reportOffsetService = new ReportOffsetFromPlanService(spendingGroupRepoMock.Object); + + Assert.ThrowsAsync(() => reportOffsetService.GetReportData(new())); + } + + [Fact] + public async Task GetReportData_WhenSpendingGroupHasChangeRecords_ThenReturnsSpendingGroupViewModel() + { + var spendingGroupRepoMock = new Mock(); + var spendingGroup = new SpendingGroupDto() + { + ChangeRecords = + [ + new() + { + Id = Guid.NewGuid(), + ChangedAt = DateTime.Now + } + ] + }; + spendingGroupRepoMock.Setup(repo => repo.GetByPlan(It.IsAny())).ReturnsAsync(spendingGroup); + var reportOffsetService = new ReportOffsetFromPlanService(spendingGroupRepoMock.Object); + + var result = await reportOffsetService.GetReportData(new()); + + Assert.NotNull(result); + Assert.IsType(result); + } +} \ No newline at end of file diff --git a/back/Services.Tests/Reports/ReportPeriodServiceTests.cs b/back/Services.Tests/Reports/ReportPeriodServiceTests.cs new file mode 100644 index 0000000..5b7fc35 --- /dev/null +++ b/back/Services.Tests/Reports/ReportPeriodServiceTests.cs @@ -0,0 +1,41 @@ +using Contracts.DTO; +using Contracts.Repositories; +using Contracts.SearchModels; +using Contracts.ViewModels; +using Moq; +using Services.Reports; +using Services.Support.Exceptions; + +namespace Services.Tests.Reports; + +public class ReportPeriodServiceTests +{ + [Fact] + public void GetReportData_WhenChangeRecordsNotFound_ThenThrowsReportDataNotFoundException() + { + var changeRecordRepoMock = new Mock(); + changeRecordRepoMock.Setup(repo => repo.GetList(It.IsAny())).ReturnsAsync((List)null); + var reportPeriodService = new ReportPeriodService(changeRecordRepoMock.Object); + + Assert.ThrowsAsync(() => reportPeriodService.GetReportData(DateTime.MinValue, DateTime.MaxValue)); + } + + [Fact] + public async Task GetReportData_WhenChangeRecordsFound_ThenReturnsChangeRecordViewModels() + { + var changeRecordRepoMock = new Mock(); + var changeRecords = new List() + { + new() { Id = Guid.NewGuid(), ChangedAt = DateTime.Now }, + new() { Id = Guid.NewGuid(), ChangedAt = DateTime.Now } + }; + changeRecordRepoMock.Setup(repo => repo.GetList(It.IsAny())).ReturnsAsync(changeRecords); + var reportPeriodService = new ReportPeriodService(changeRecordRepoMock.Object); + + var result = await reportPeriodService.GetReportData(DateTime.MinValue, DateTime.MaxValue); + + changeRecordRepoMock.Verify(repo => repo.GetList(It.IsAny()), Times.Once); + Assert.NotNull(result); + Assert.IsType>(result.ToList()); + } +} \ No newline at end of file diff --git a/back/Services/Reports/ReportOffsetFromPlanService.cs b/back/Services/Reports/ReportOffsetFromPlanService.cs new file mode 100644 index 0000000..d2a66b6 --- /dev/null +++ b/back/Services/Reports/ReportOffsetFromPlanService.cs @@ -0,0 +1,32 @@ +using Contracts.Mappers; +using Contracts.Repositories; +using Contracts.SearchModels; +using Contracts.Services; +using Contracts.ViewModels; +using Services.Support.Exceptions; + +namespace Services.Reports; + +public class ReportOffsetFromPlanService : IReportOffsetFromPlanService +{ + private readonly ISpendingGroupRepo _spendingGroupRepo; + + public ReportOffsetFromPlanService(ISpendingGroupRepo spendingGroupRepo) + { + _spendingGroupRepo = spendingGroupRepo; + } + + public async Task GetReportData(SpendingPlanSearch search) + { + var group = await _spendingGroupRepo.GetByPlan(search); + if (group == null) + { + throw new EntityNotFoundException("Не удалось найти группу по такому плану"); + } + if (!group.ChangeRecords.Any()) + { + throw new ReportDataNotFoundException("Данные об изменении баланса отсутствуют"); + } + return group.ToView(); + } +} diff --git a/back/Services/Reports/ReportPeriodService.cs b/back/Services/Reports/ReportPeriodService.cs new file mode 100644 index 0000000..ea2d246 --- /dev/null +++ b/back/Services/Reports/ReportPeriodService.cs @@ -0,0 +1,29 @@ +using Contracts.Mappers; +using Contracts.Repositories; +using Contracts.SearchModels; +using Contracts.Services; +using Contracts.ViewModels; +using Services.Support.Exceptions; + +namespace Services.Reports; + +public class ReportPeriodService : IReportPeriodService +{ + private readonly IChangeRecordRepo _changeRecordRepo; + + public ReportPeriodService(IChangeRecordRepo changeRecordRepo) + { + _changeRecordRepo = changeRecordRepo; + } + + public async Task> GetReportData(DateTime from, DateTime to, Guid userId) + { + var records = await _changeRecordRepo.GetList(new ChangeRecordSearch() { From = from, To = to, UserId = userId }); + + if (!records.Any()) + { + throw new ReportDataNotFoundException("Нет данных за указанный период"); + } + return records.Select(x => x.ToView()).ToList(); + } +} diff --git a/back/Services/Support/Exceptions/ReportDataNotFoundException.cs b/back/Services/Support/Exceptions/ReportDataNotFoundException.cs new file mode 100644 index 0000000..ffdd60e --- /dev/null +++ b/back/Services/Support/Exceptions/ReportDataNotFoundException.cs @@ -0,0 +1,10 @@ +namespace Services.Support.Exceptions; + +public class ReportDataNotFoundException : EntityNotFoundException +{ + public ReportDataNotFoundException(string message) + : base(message) { } + public ReportDataNotFoundException(string message, Exception innerException) + : base(message, innerException) { } + +} \ No newline at end of file diff --git a/docker-compose.debug.yml b/docker-compose.debug.yml index 02d8292..1d23e24 100644 --- a/docker-compose.debug.yml +++ b/docker-compose.debug.yml @@ -35,6 +35,8 @@ services: - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data + ports: + - 5432:5432 volumes: postgres_data: diff --git a/front/components.d.ts b/front/components.d.ts index 55d7595..5fdf850 100644 --- a/front/components.d.ts +++ b/front/components.d.ts @@ -9,6 +9,7 @@ declare module 'vue' { export interface GlobalComponents { AButton: typeof import('ant-design-vue/es')['Button'] ADatePicker: typeof import('ant-design-vue/es')['DatePicker'] + ADivider: typeof import('ant-design-vue/es')['Divider'] AForm: typeof import('ant-design-vue/es')['Form'] AFormItem: typeof import('ant-design-vue/es')['FormItem'] AInput: typeof import('ant-design-vue/es')['Input'] @@ -18,16 +19,22 @@ declare module 'vue' { ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent'] ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader'] APopconfirm: typeof import('ant-design-vue/es')['Popconfirm'] + ARangePicker: typeof import('ant-design-vue/es')['RangePicker'] ASelect: typeof import('ant-design-vue/es')['Select'] ASelectOption: typeof import('ant-design-vue/es')['SelectOption'] ASpace: typeof import('ant-design-vue/es')['Space'] ASpin: typeof import('ant-design-vue/es')['Spin'] ATable: typeof import('ant-design-vue/es')['Table'] + ATypographyText: typeof import('ant-design-vue/es')['TypographyText'] + ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle'] ChangeRecordManager: typeof import('./src/components/support/ChangeRecordManager.vue')['default'] + GetPeriodReportForm: typeof import('./src/components/support/GetPeriodReportForm.vue')['default'] Groups: typeof import('./src/components/pages/Groups.vue')['default'] Header: typeof import('./src/components/main/Header.vue')['default'] Home: typeof import('./src/components/pages/Home.vue')['default'] Login: typeof import('./src/components/pages/Login.vue')['default'] + OffsetReport: typeof import('./src/components/pages/OffsetReport.vue')['default'] + PeriodReport: typeof import('./src/components/pages/PeriodReport.vue')['default'] PlanManager: typeof import('./src/components/support/PlanManager.vue')['default'] Plans: typeof import('./src/components/pages/Plans.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] diff --git a/front/package-lock.json b/front/package-lock.json index c1ae6fb..6ec091f 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -13,7 +13,8 @@ "dayjs": "^1.11.13", "pinia": "^2.2.8", "vue": "^3.5.12", - "vue-router": "^4.5.0" + "vue-router": "^4.5.0", + "vue3-charts": "^1.1.33" }, "devDependencies": { "@vitejs/plugin-vue": "^5.1.4", @@ -1443,6 +1444,136 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", @@ -1726,6 +1857,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -2308,6 +2448,16 @@ ], "license": "MIT" }, + "node_modules/ramda": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.28.0.tgz", + "integrity": "sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ramda" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -2895,6 +3045,28 @@ "vue": "^3.0.0" } }, + "node_modules/vue3-charts": { + "version": "1.1.33", + "resolved": "https://registry.npmjs.org/vue3-charts/-/vue3-charts-1.1.33.tgz", + "integrity": "sha512-gu2N/oORcAWLo3orfoKz5CRohZdmxQP7k2SZ8cgRsD9hFmYpekesE41EUPdGuZ5Y9gAo2LbGYW7fmIGbbPezDg==", + "dependencies": { + "d3-array": "^3.2.0", + "d3-axis": "^3.0.0", + "d3-format": "^3.1.0", + "d3-hierarchy": "^3.1.2", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-selection": "^3.0.0", + "d3-shape": "^3.1.0", + "ramda": "^0.28.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "vue": ">=3.0.0" + } + }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", diff --git a/front/package.json b/front/package.json index 9dae701..6552556 100644 --- a/front/package.json +++ b/front/package.json @@ -15,7 +15,8 @@ "dayjs": "^1.11.13", "pinia": "^2.2.8", "vue": "^3.5.12", - "vue-router": "^4.5.0" + "vue-router": "^4.5.0", + "vue3-charts": "^1.1.33" }, "devDependencies": { "@vitejs/plugin-vue": "^5.1.4", diff --git a/front/src/components/main/Header.vue b/front/src/components/main/Header.vue index d2d0563..063d911 100644 --- a/front/src/components/main/Header.vue +++ b/front/src/components/main/Header.vue @@ -3,6 +3,7 @@ import { inject } from 'vue'; import { useUserStore } from '../../store'; import { AuthService } from '../../core/services/auth-service'; import router from '../../router'; +import { HomeOutlined, BlockOutlined } from '@ant-design/icons-vue'; const store = useUserStore(); const authService = inject(AuthService.name) as AuthService; @@ -19,8 +20,8 @@ function logout() {
ДомБюдж
diff --git a/front/src/components/pages/Groups.vue b/front/src/components/pages/Groups.vue index 8781ced..43b2c9b 100644 --- a/front/src/components/pages/Groups.vue +++ b/front/src/components/pages/Groups.vue @@ -3,7 +3,7 @@ import { useAsyncState } from '@vueuse/core'; import { inject } from 'vue'; import { GroupService } from '../../core/services/group-service'; import SpendingGroupManager from '../support/SpendingGroupManager.vue'; -import { DeleteOutlined } from '@ant-design/icons-vue'; +import { DeleteOutlined, CalendarOutlined } from '@ant-design/icons-vue'; const groupService = inject(GroupService.name) as GroupService; @@ -43,12 +43,15 @@ const onDelete = (key: string) => {