0.2.0 #4
25
.dockerignore
Normal file
25
.dockerignore
Normal file
@ -0,0 +1,25 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
5
.env
Normal file
5
.env
Normal file
@ -0,0 +1,5 @@
|
||||
POSTGRES_DB="dombudg"
|
||||
POSTGRES_USER="postgres"
|
||||
POSTGRES_PASSWORD="postgres"
|
||||
|
||||
CONNECTION_STRING="Host=database:5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD};"
|
39
.vscode/launch.json
vendored
Normal file
39
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": ".NET Core Launch (web)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "${workspaceFolder}/back/Controllers/bin/Debug/net8.0/Controllers.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/back/Controllers",
|
||||
"stopAtEntry": false,
|
||||
"serverReadyAction": {
|
||||
"action": "openExternally",
|
||||
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
|
||||
},
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"sourceFileMap": {
|
||||
"/Views": "${workspaceFolder}/Views"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": ".NET Core Attach",
|
||||
"type": "coreclr",
|
||||
"request": "attach"
|
||||
},
|
||||
{
|
||||
"name": "Docker .NET Launch",
|
||||
"type": "docker",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "docker-run: debug",
|
||||
"netCore": {
|
||||
"appProject": "${workspaceFolder}/back/Controllers/Controllers.csproj"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
101
.vscode/tasks.json
vendored
Normal file
101
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,101 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/back/Api.sln",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "publish",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"publish",
|
||||
"${workspaceFolder}/back/Api.sln",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "watch",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/back/Api.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"type": "docker-build",
|
||||
"label": "docker-build: debug",
|
||||
"dependsOn": [
|
||||
"build"
|
||||
],
|
||||
"dockerBuild": {
|
||||
"tag": "dombudg:dev",
|
||||
"target": "base",
|
||||
"dockerfile": "${workspaceFolder}/back/Controllers/Dockerfile",
|
||||
"context": "${workspaceFolder}",
|
||||
"pull": true
|
||||
},
|
||||
"netCore": {
|
||||
"appProject": "${workspaceFolder}/back/Controllers/Controllers.csproj"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "docker-build",
|
||||
"label": "docker-build: release",
|
||||
"dependsOn": [
|
||||
"build"
|
||||
],
|
||||
"dockerBuild": {
|
||||
"tag": "dombudg:latest",
|
||||
"dockerfile": "${workspaceFolder}/back/Controllers/Dockerfile",
|
||||
"context": "${workspaceFolder}",
|
||||
"platform": {
|
||||
"os": "linux",
|
||||
"architecture": "amd64"
|
||||
},
|
||||
"pull": true
|
||||
},
|
||||
"netCore": {
|
||||
"appProject": "${workspaceFolder}/back/Controllers/Controllers.csproj"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "docker-run",
|
||||
"label": "docker-run: debug",
|
||||
"dependsOn": [
|
||||
"docker-build: debug"
|
||||
],
|
||||
"dockerRun": {},
|
||||
"netCore": {
|
||||
"appProject": "${workspaceFolder}/back/Controllers/Controllers.csproj",
|
||||
"enableDebugging": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "docker-run",
|
||||
"label": "docker-run: release",
|
||||
"dependsOn": [
|
||||
"docker-build: release"
|
||||
],
|
||||
"dockerRun": {},
|
||||
"netCore": {
|
||||
"appProject": "${workspaceFolder}/back/Controllers/Controllers.csproj"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Contracts", "Contracts\Cont
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "Infrastructure\Infrastructure.csproj", "{A35121D4-7D41-4266-8DA4-87135E8ABF89}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Services.Tests", "Services.Tests\Services.Tests.csproj", "{F0BDEEB0-2850-4733-B196-DBC677B35E47}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -36,5 +38,9 @@ Global
|
||||
{A35121D4-7D41-4266-8DA4-87135E8ABF89}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A35121D4-7D41-4266-8DA4-87135E8ABF89}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A35121D4-7D41-4266-8DA4-87135E8ABF89}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F0BDEEB0-2850-4733-B196-DBC677B35E47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F0BDEEB0-2850-4733-B196-DBC677B35E47}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F0BDEEB0-2850-4733-B196-DBC677B35E47}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F0BDEEB0-2850-4733-B196-DBC677B35E47}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
@ -5,7 +5,7 @@ public class ChangeRecordDto
|
||||
public Guid Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public Guid? SpendingGroupId { get; set; }
|
||||
public string SpendingGroupName { get; set; } = string.Empty;
|
||||
public string? SpendingGroupName { get; set; }
|
||||
public decimal Sum { get; set; }
|
||||
public DateTime ChangedAt { get; set; }
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
namespace Contracts.DTO;
|
||||
|
||||
public class UserLoginDTO
|
||||
public class UserLoginDto
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
@ -9,8 +9,8 @@ public static class ChangeRecordMapper
|
||||
=> new()
|
||||
{
|
||||
Id = dto.Id,
|
||||
Sum = dto.Sum,
|
||||
ChangedAt = dto.ChangedAt,
|
||||
SpendingGroupName = dto.SpendingGroupName
|
||||
Sum = dto.Sum,
|
||||
ChangedAt = dto.ChangedAt.ToString("dd.MM.yyyy"),
|
||||
SpendingGroupName = dto.SpendingGroupName ?? string.Empty
|
||||
};
|
||||
}
|
@ -9,8 +9,8 @@ public static class SpendingPlanMapper
|
||||
=> new()
|
||||
{
|
||||
Id = dto.Id,
|
||||
StartAt = dto.StartAt,
|
||||
EndAt = dto.EndAt,
|
||||
StartAt = dto.StartAt.ToString("dd.MM.yyyy"),
|
||||
EndAt = dto.EndAt.ToString("dd.MM.yyyy"),
|
||||
Sum = dto.Sum
|
||||
};
|
||||
}
|
@ -6,4 +6,5 @@ public class ChangeRecordSearch
|
||||
public Guid? SpendingGroupId { get; set; }
|
||||
public DateTime? From { get; set; }
|
||||
public DateTime? To { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
}
|
@ -4,4 +4,5 @@ public class SpendingGroupSearch
|
||||
{
|
||||
public Guid? Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
}
|
@ -6,6 +6,6 @@ namespace Contracts.Services;
|
||||
|
||||
public interface IAuthService
|
||||
{
|
||||
public Task<UserViewModel> Login(UserLoginDTO loginData);
|
||||
public Task<UserViewModel> Login(UserLoginDto loginData);
|
||||
public Task<UserViewModel> Register(UserDto user);
|
||||
}
|
@ -4,6 +4,6 @@ public class ChangeRecordViewModel
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public decimal Sum { get; set; }
|
||||
public DateTime ChangedAt { get; set; }
|
||||
public string ChangedAt { get; set; } = null!;
|
||||
public string SpendingGroupName { get; set; } = string.Empty;
|
||||
}
|
@ -3,7 +3,7 @@ namespace Contracts.ViewModels;
|
||||
public class SpendingPlanViewModel
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public DateTime StartAt { get; set; }
|
||||
public DateTime EndAt { get; set; }
|
||||
public string StartAt { get; set; } = null!;
|
||||
public string EndAt { get; set; } = null!;
|
||||
public decimal Sum { get; set; }
|
||||
}
|
@ -18,7 +18,7 @@ public class AuthController : ControllerBase
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<UserViewModel>> Login([FromBody] UserLoginDTO loginData)
|
||||
public async Task<ActionResult<UserViewModel>> Login([FromBody] UserLoginDto loginData)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
24
back/Controllers/Dockerfile
Normal file
24
back/Controllers/Dockerfile
Normal file
@ -0,0 +1,24 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 5125
|
||||
|
||||
ENV ASPNETCORE_URLS=http://+:5125
|
||||
|
||||
USER app
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
ARG configuration=Release
|
||||
WORKDIR /src
|
||||
COPY ["back/Controllers/Controllers.csproj", "back/Controllers/"]
|
||||
RUN dotnet restore "back/Controllers/Controllers.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/back/Controllers"
|
||||
RUN dotnet build "Controllers.csproj" -c $configuration -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
ARG configuration=Release
|
||||
RUN dotnet publish "Controllers.csproj" -c $configuration -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "Controllers.dll"]
|
@ -9,6 +9,7 @@ public static class DatabaseSetupExtension
|
||||
{
|
||||
var connectionString = config.GetConnectionString("DefaultConnection")
|
||||
?? throw new ArgumentException("Нет строки подключения");
|
||||
Console.WriteLine("Connection string: " + connectionString);
|
||||
services.AddDbContext<DatabaseContext>(options => options.UseNpgsql(connectionString));
|
||||
services.AddSingleton<IDbContextFactory<DatabaseContext>, DbContextFactory>();
|
||||
}
|
||||
|
@ -21,9 +21,10 @@ if (app.Environment.IsDevelopment())
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.MigrateDb();
|
||||
|
||||
app.UseCors(builder => builder.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
@ -71,6 +71,10 @@ public class ChangeRecordRepo : IChangeRecordRepo
|
||||
{
|
||||
query = query.Where(x => x.ChangedAt >= search.From && x.ChangedAt <= search.To);
|
||||
}
|
||||
if (search.UserId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.UserId == search.UserId);
|
||||
}
|
||||
}
|
||||
return await query.Include(x => x.SpendingGroup).Select(x => x.ToDto()).ToListAsync();
|
||||
}
|
||||
|
@ -50,7 +50,9 @@ public class SpendingGroupRepo : ISpendingGroupRepo
|
||||
.Include(x => x.ChangeRecords)
|
||||
.Include(x => x.SpendingPlans)
|
||||
.FirstOrDefaultAsync(x => x.Id == search.Id
|
||||
|| x.Name == search.Name);
|
||||
|| (!string.IsNullOrWhiteSpace(search.Name)
|
||||
&& x.Name == search.Name
|
||||
&& x.UserId == search.UserId));
|
||||
|
||||
return group?.ToDto();
|
||||
}
|
||||
@ -100,9 +102,10 @@ public class SpendingGroupRepo : ISpendingGroupRepo
|
||||
query = query.Where(x => x.Id == search.Id);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search.Name))
|
||||
if (!string.IsNullOrWhiteSpace(search.Name) && search.UserId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.Name.Contains(search.Name, StringComparison.OrdinalIgnoreCase));
|
||||
query = query.Where(x => x.Name.Contains(search.Name, StringComparison.OrdinalIgnoreCase)
|
||||
&& x.UserId == search.UserId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ public static class ChangeRecordMapper
|
||||
ChangedAt = changeRecord.ChangedAt,
|
||||
SpendingGroupId = changeRecord.SpendingGroupId,
|
||||
UserId = changeRecord.UserId,
|
||||
SpendingGroupName = changeRecord.SpendingGroup?.Name ?? string.Empty
|
||||
SpendingGroupName = changeRecord.SpendingGroup?.Name
|
||||
};
|
||||
|
||||
public static ChangeRecord ToModel(this ChangeRecordDto changeRecord)
|
||||
|
83
back/Services.Tests/Domain/AuthServiceTests.cs
Normal file
83
back/Services.Tests/Domain/AuthServiceTests.cs
Normal file
@ -0,0 +1,83 @@
|
||||
using Contracts.DTO;
|
||||
using Contracts.Repositories;
|
||||
using Contracts.SearchModels;
|
||||
using Contracts.ViewModels;
|
||||
using Moq;
|
||||
using Services.Domain;
|
||||
using Services.Support.Exceptions;
|
||||
|
||||
namespace Services.Tests.Domain;
|
||||
|
||||
public class AuthServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void Register_WhenUserExists_ThrowsAlreadyExistsException()
|
||||
{
|
||||
var userRepoMock = new Mock<IUserRepo>();
|
||||
userRepoMock.Setup(repo => repo.Get(It.IsAny<UserSearch>()))
|
||||
.ReturnsAsync(new UserDto());
|
||||
var authService = new AuthService(userRepoMock.Object);
|
||||
var user = new UserDto { Name = "John Doe", Password = "password" };
|
||||
|
||||
Assert.ThrowsAsync<AlreadyExistsException>(() => authService.Register(user));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_WhenUserDoesNotExist_ThenCreateUser_ReturnsUserViewModel()
|
||||
{
|
||||
var userRepoMock = new Mock<IUserRepo>();
|
||||
userRepoMock.Setup(repo => repo.Get(It.IsAny<UserSearch>())).ReturnsAsync((UserDto)null);
|
||||
userRepoMock.Setup(repo => repo.Create(It.IsAny<UserDto>())).ReturnsAsync(new UserDto());
|
||||
var authService = new AuthService(userRepoMock.Object);
|
||||
var user = new UserDto { Name = "John Doe", Password = "password" };
|
||||
|
||||
var result = authService.Register(user);
|
||||
|
||||
userRepoMock.Verify(repo => repo.Create(It.IsAny<UserDto>()), Times.Once);
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<Task<UserViewModel>>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Login_WhenUserDoesNotExist_ThrowsUserNotFoundException()
|
||||
{
|
||||
var userRepoMock = new Mock<IUserRepo>();
|
||||
userRepoMock.Setup(repo => repo.Get(It.IsAny<UserSearch>())).ReturnsAsync((UserDto)null);
|
||||
var authService = new AuthService(userRepoMock.Object);
|
||||
var user = new UserLoginDto { Name = "John Doe", Password = "password" };
|
||||
|
||||
Assert.ThrowsAsync<UserNotFoundException>(() => authService.Login(user));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Login_WhenUserExists_ReturnsUserViewModel()
|
||||
{
|
||||
var userRepoMock = new Mock<IUserRepo>();
|
||||
userRepoMock.Setup(repo => repo.Get(It.IsAny<UserSearch>())).ReturnsAsync(new UserDto());
|
||||
var authService = new AuthService(userRepoMock.Object);
|
||||
var user = new UserLoginDto { Name = "John Doe", Password = "password" };
|
||||
|
||||
var result = authService.Login(user);
|
||||
|
||||
userRepoMock.Verify(repo => repo.Get(It.IsAny<UserSearch>()), Times.Once);
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<Task<UserViewModel>>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Login_WhenWrongLoginData_ThrowsArgumentException()
|
||||
{
|
||||
var userRepoMock = new Mock<IUserRepo>();
|
||||
userRepoMock.Setup(repo => repo.Get(It.IsAny<UserSearch>())).ReturnsAsync((UserDto)null);
|
||||
var authService = new AuthService(userRepoMock.Object);
|
||||
UserLoginDto user1 = null;
|
||||
UserLoginDto user2 = new() { Name = "", Password = "password" };
|
||||
UserLoginDto user3 = new() { Name = "John Doe", Password = "" };
|
||||
UserLoginDto user4 = new() { Name = "", Password = "" };
|
||||
|
||||
Assert.ThrowsAsync<ArgumentException>(() => authService.Login(user1));
|
||||
Assert.ThrowsAsync<ArgumentException>(() => authService.Login(user2));
|
||||
Assert.ThrowsAsync<ArgumentException>(() => authService.Login(user3));
|
||||
Assert.ThrowsAsync<ArgumentException>(() => authService.Login(user4));
|
||||
}
|
||||
}
|
138
back/Services.Tests/Domain/ChangeRecordServiceTests.cs
Normal file
138
back/Services.Tests/Domain/ChangeRecordServiceTests.cs
Normal file
@ -0,0 +1,138 @@
|
||||
using Contracts.DTO;
|
||||
using Contracts.Repositories;
|
||||
using Contracts.SearchModels;
|
||||
using Contracts.ViewModels;
|
||||
using Moq;
|
||||
using Services.Domain;
|
||||
using Services.Support.Exceptions;
|
||||
using Services.Tests.Support.Fakes.Database;
|
||||
using Services.Tests.Support.Fakes.Repositories;
|
||||
|
||||
namespace Services.Tests.Domain;
|
||||
|
||||
public class ChangeRecordServiceTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(50)]
|
||||
[InlineData(-50)]
|
||||
public async Task Create_WhenUserExists_ThenChangeBalance_ReturnsChangeRecordViewModel(int recordSum, int userBalance = 100)
|
||||
{
|
||||
var userRepoFake = new UserRepoFake();
|
||||
var changeRecordRepoMock = new Mock<IChangeRecordRepo>();
|
||||
changeRecordRepoMock.Setup(repo => repo.Create(It.IsAny<ChangeRecordDto>())).ReturnsAsync(new ChangeRecordDto());
|
||||
var changeRecordService = new ChangeRecordService(changeRecordRepoMock.Object, userRepoFake);
|
||||
// Add user
|
||||
var user = new UserDto() { Id = Guid.NewGuid(), Balance = userBalance };
|
||||
await userRepoFake.Create(user);
|
||||
var changeRecord = new ChangeRecordDto { UserId = user.Id, Sum = recordSum, SpendingGroupId = Guid.NewGuid() };
|
||||
|
||||
var result = await changeRecordService.Create(changeRecord);
|
||||
|
||||
changeRecordRepoMock.Verify(repo => repo.Create(It.IsAny<ChangeRecordDto>()), Times.Once);
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<ChangeRecordViewModel>(result);
|
||||
var existedUser = await userRepoFake.Get(new UserSearch { Id = user.Id });
|
||||
Assert.Equal(existedUser.Balance, userBalance + recordSum);
|
||||
|
||||
DbFake.ClearDb();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(50)]
|
||||
[InlineData(-50)]
|
||||
public async Task Delete_WhenUserExists_ThenChangeBalance_ReturnsChangeRecordViewModel(int recordSum, int userBalance = 100)
|
||||
{
|
||||
var user = new UserDto() { Id = Guid.NewGuid(), Balance = userBalance };
|
||||
var changeRecord = new ChangeRecordDto { UserId = user.Id, Sum = recordSum, SpendingGroupId = Guid.NewGuid() };
|
||||
var userRepoFake = new UserRepoFake();
|
||||
var changeRecordRepoMock = new Mock<IChangeRecordRepo>();
|
||||
changeRecordRepoMock.Setup(repo => repo.Delete(It.IsAny<ChangeRecordSearch>())).ReturnsAsync(changeRecord);
|
||||
changeRecordRepoMock.Setup(repo => repo.Create(It.IsAny<ChangeRecordDto>())).ReturnsAsync(new ChangeRecordDto());
|
||||
var changeRecordService = new ChangeRecordService(changeRecordRepoMock.Object, userRepoFake);
|
||||
// Add user and record
|
||||
await userRepoFake.Create(user);
|
||||
await changeRecordService.Create(changeRecord);
|
||||
|
||||
var result = await changeRecordService.Delete(new ChangeRecordSearch { Id = changeRecord.Id });
|
||||
|
||||
changeRecordRepoMock.Verify(repo => repo.Delete(It.IsAny<ChangeRecordSearch>()), Times.Once);
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<ChangeRecordViewModel>(result);
|
||||
var existedUser = await userRepoFake.Get(new UserSearch { Id = user.Id });
|
||||
Assert.Equal(existedUser.Balance, userBalance);
|
||||
|
||||
DbFake.ClearDb();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Delete_WhenRecordNotFound_ThenThrowsEntityNotFoundException()
|
||||
{
|
||||
var changeRecordRepoMock = new Mock<IChangeRecordRepo>();
|
||||
var userRepoMock = new Mock<IUserRepo>();
|
||||
changeRecordRepoMock.Setup(repo => repo.Delete(It.IsAny<ChangeRecordSearch>())).ReturnsAsync((ChangeRecordDto)null);
|
||||
var changeRecordService = new ChangeRecordService(changeRecordRepoMock.Object, userRepoMock.Object);
|
||||
|
||||
Assert.ThrowsAsync<EntityNotFoundException>(() => changeRecordService.Delete(new()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetList_ReturnsChangeRecordViewModels()
|
||||
{
|
||||
var changeRecordRepoMock = new Mock<IChangeRecordRepo>();
|
||||
var userRepoMock = new Mock<IUserRepo>();
|
||||
changeRecordRepoMock.Setup(repo => repo.GetList(It.IsAny<ChangeRecordSearch>())).ReturnsAsync(GetAllChangeRecords());
|
||||
var changeRecordService = new ChangeRecordService(changeRecordRepoMock.Object, userRepoMock.Object);
|
||||
|
||||
var result = await changeRecordService.GetList();
|
||||
|
||||
changeRecordRepoMock.Verify(repo => repo.GetList(It.IsAny<ChangeRecordSearch>()), Times.Once);
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<List<ChangeRecordViewModel>>(result.ToList());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(50, 25)]
|
||||
[InlineData(-50, 25)]
|
||||
public async Task Update_WhenUserExists_ThenChangeBalance_ReturnsChangeRecordViewModel(int recordSum, int recorNewSum, int userBalance = 100)
|
||||
{
|
||||
var user = new UserDto() { Id = Guid.NewGuid(), Balance = userBalance };
|
||||
var changeRecord = new ChangeRecordDto { UserId = user.Id, Sum = recordSum, SpendingGroupId = Guid.NewGuid() };
|
||||
var userRepoFake = new UserRepoFake();
|
||||
var changeRecordRepoMock = new Mock<IChangeRecordRepo>();
|
||||
changeRecordRepoMock.Setup(repo => repo.Update(It.IsAny<ChangeRecordDto>())).ReturnsAsync(changeRecord);
|
||||
changeRecordRepoMock.Setup(repo => repo.Create(It.IsAny<ChangeRecordDto>())).ReturnsAsync(new ChangeRecordDto());
|
||||
var changeRecordService = new ChangeRecordService(changeRecordRepoMock.Object, userRepoFake);
|
||||
// Add user and record
|
||||
await userRepoFake.Create(user);
|
||||
await changeRecordService.Create(changeRecord);
|
||||
var newChangeRecord = new ChangeRecordDto { Id = Guid.NewGuid(), UserId = user.Id, Sum = recorNewSum, SpendingGroupId = Guid.NewGuid() };
|
||||
|
||||
var result = await changeRecordService.Update(newChangeRecord);
|
||||
|
||||
changeRecordRepoMock.Verify(repo => repo.Update(It.IsAny<ChangeRecordDto>()), Times.Once);
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<ChangeRecordViewModel>(result);
|
||||
var existedUser = await userRepoFake.Get(new UserSearch { Id = user.Id });
|
||||
Assert.Equal(existedUser.Balance, userBalance + recorNewSum);
|
||||
|
||||
DbFake.ClearDb();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WhenRecordNotExists_ThenThrowsEntityNotFoundException()
|
||||
{
|
||||
var changeRecordRepoMock = new Mock<IChangeRecordRepo>();
|
||||
changeRecordRepoMock.Setup(repo => repo.Update(It.IsAny<ChangeRecordDto>())).ReturnsAsync((ChangeRecordDto?)null);
|
||||
var userRepoMock = new Mock<IUserRepo>();
|
||||
var changeRecordService = new ChangeRecordService(changeRecordRepoMock.Object, userRepoMock.Object);
|
||||
var changeRecord = new ChangeRecordDto { Id = Guid.NewGuid(), UserId = Guid.NewGuid(), Sum = 50, SpendingGroupId = Guid.NewGuid() };
|
||||
|
||||
Assert.ThrowsAsync<EntityNotFoundException>(() => changeRecordService.Update(changeRecord));
|
||||
}
|
||||
|
||||
public IEnumerable<ChangeRecordDto> GetAllChangeRecords() => new List<ChangeRecordDto>()
|
||||
{
|
||||
new ChangeRecordDto() { Id = Guid.NewGuid(), Sum = 50, SpendingGroupId = Guid.NewGuid(), UserId = Guid.NewGuid() },
|
||||
new ChangeRecordDto() { Id = Guid.NewGuid(), Sum = 50, SpendingGroupId = Guid.NewGuid(), UserId = Guid.NewGuid() },
|
||||
};
|
||||
}
|
118
back/Services.Tests/Domain/SpendingGroupServiceTests.cs
Normal file
118
back/Services.Tests/Domain/SpendingGroupServiceTests.cs
Normal file
@ -0,0 +1,118 @@
|
||||
using Contracts.DTO;
|
||||
using Contracts.Repositories;
|
||||
using Contracts.SearchModels;
|
||||
using Contracts.Services;
|
||||
using Contracts.ViewModels;
|
||||
using Moq;
|
||||
using Services.Support.Exceptions;
|
||||
|
||||
namespace Services.Tests.Domain;
|
||||
|
||||
public class SpendingGroupServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Create_ReturnsSpendingGroupViewModel()
|
||||
{
|
||||
var spendingGroupRepoMock = new Mock<ISpendingGroupRepo>();
|
||||
spendingGroupRepoMock.Setup(repo => repo.Create(It.IsAny<SpendingGroupDto>())).ReturnsAsync(new SpendingGroupDto());
|
||||
var spendingGroupService = new SpendingGroupService(spendingGroupRepoMock.Object);
|
||||
|
||||
var result = await spendingGroupService.Create(new());
|
||||
|
||||
spendingGroupRepoMock.Verify(repo => repo.Create(It.IsAny<SpendingGroupDto>()), Times.Once);
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<SpendingGroupViewModel>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetList_ReturnsSpendingGroupViewModels()
|
||||
{
|
||||
var spendingGroupRepoMock = new Mock<ISpendingGroupRepo>();
|
||||
spendingGroupRepoMock.Setup(repo => repo.GetList(It.IsAny<SpendingGroupSearch>())).ReturnsAsync(_getAllSpendingGroups());
|
||||
var spendingGroupService = new SpendingGroupService(spendingGroupRepoMock.Object);
|
||||
|
||||
var result = await spendingGroupService.GetList();
|
||||
|
||||
spendingGroupRepoMock.Verify(repo => repo.GetList(It.IsAny<SpendingGroupSearch>()), Times.Once);
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<List<SpendingGroupViewModel>>(result.ToList());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_ReturnsSpendingGroupViewModel()
|
||||
{
|
||||
var spendingGroupRepoMock = new Mock<ISpendingGroupRepo>();
|
||||
spendingGroupRepoMock.Setup(repo => repo.Delete(It.IsAny<SpendingGroupSearch>())).ReturnsAsync(new SpendingGroupDto());
|
||||
var spendingGroupService = new SpendingGroupService(spendingGroupRepoMock.Object);
|
||||
|
||||
var result = await spendingGroupService.Delete(new());
|
||||
|
||||
spendingGroupRepoMock.Verify(repo => repo.Delete(It.IsAny<SpendingGroupSearch>()), Times.Once);
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<SpendingGroupViewModel>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Delete_WhenSpendingGroupNotFound_ThrowsEntityNotFoundException()
|
||||
{
|
||||
var spendingGroupRepoMock = new Mock<ISpendingGroupRepo>();
|
||||
spendingGroupRepoMock.Setup(repo => repo.Delete(It.IsAny<SpendingGroupSearch>())).ReturnsAsync((SpendingGroupDto)null);
|
||||
var spendingGroupService = new SpendingGroupService(spendingGroupRepoMock.Object);
|
||||
|
||||
Assert.ThrowsAsync<EntityNotFoundException>(() => spendingGroupService.Delete(new()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Update_ReturnsSpendingGroupViewModel()
|
||||
{
|
||||
var spendingGroupRepoMock = new Mock<ISpendingGroupRepo>();
|
||||
spendingGroupRepoMock.Setup(repo => repo.Update(It.IsAny<SpendingGroupDto>())).ReturnsAsync(new SpendingGroupDto());
|
||||
var spendingGroupService = new SpendingGroupService(spendingGroupRepoMock.Object);
|
||||
|
||||
var result = await spendingGroupService.Update(new());
|
||||
|
||||
spendingGroupRepoMock.Verify(repo => repo.Update(It.IsAny<SpendingGroupDto>()), Times.Once);
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<SpendingGroupViewModel>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WhenSpendingGroupNotFound_ThrowsEntityNotFoundException()
|
||||
{
|
||||
var spendingGroupRepoMock = new Mock<ISpendingGroupRepo>();
|
||||
spendingGroupRepoMock.Setup(repo => repo.Update(It.IsAny<SpendingGroupDto>())).ReturnsAsync((SpendingGroupDto)null);
|
||||
var spendingGroupService = new SpendingGroupService(spendingGroupRepoMock.Object);
|
||||
|
||||
Assert.ThrowsAsync<EntityNotFoundException>(() => spendingGroupService.Update(new()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetDetails_ReturnsSpendingGroupViewModel()
|
||||
{
|
||||
var spendingGroupRepoMock = new Mock<ISpendingGroupRepo>();
|
||||
spendingGroupRepoMock.Setup(repo => repo.Get(It.IsAny<SpendingGroupSearch>())).ReturnsAsync(new SpendingGroupDto());
|
||||
var spendingGroupService = new SpendingGroupService(spendingGroupRepoMock.Object);
|
||||
|
||||
var result = await spendingGroupService.GetDetails(new());
|
||||
|
||||
spendingGroupRepoMock.Verify(repo => repo.Get(It.IsAny<SpendingGroupSearch>()), Times.Once);
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<SpendingGroupViewModel>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDetails_WhenSpendingGroupNotFound_ThrowsEntityNotFoundException()
|
||||
{
|
||||
var spendingGroupRepoMock = new Mock<ISpendingGroupRepo>();
|
||||
spendingGroupRepoMock.Setup(repo => repo.Get(It.IsAny<SpendingGroupSearch>())).ReturnsAsync((SpendingGroupDto)null);
|
||||
var spendingGroupService = new SpendingGroupService(spendingGroupRepoMock.Object);
|
||||
|
||||
Assert.ThrowsAsync<EntityNotFoundException>(() => spendingGroupService.GetDetails(new()));
|
||||
}
|
||||
|
||||
private IEnumerable<SpendingGroupDto> _getAllSpendingGroups() => new List<SpendingGroupDto>()
|
||||
{
|
||||
new() { Id = Guid.NewGuid(), Name = "Group 1" },
|
||||
new() { Id = Guid.NewGuid(), Name = "Group 2" }
|
||||
};
|
||||
}
|
101
back/Services.Tests/Domain/SpendingPlanServiceTests.cs
Normal file
101
back/Services.Tests/Domain/SpendingPlanServiceTests.cs
Normal file
@ -0,0 +1,101 @@
|
||||
using Contracts.DTO;
|
||||
using Contracts.Repositories;
|
||||
using Contracts.SearchModels;
|
||||
using Contracts.ViewModels;
|
||||
using Moq;
|
||||
using Services.Domain;
|
||||
using Services.Support.Exceptions;
|
||||
|
||||
namespace Services.Tests.Domain;
|
||||
|
||||
public class SpendingPlanServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Create_ReturnsSpendingPlanViewModel()
|
||||
{
|
||||
var spendingPlanRepoMock = new Mock<ISpendingPlanRepo>();
|
||||
spendingPlanRepoMock.Setup(repo => repo.Create(It.IsAny<SpendingPlanDto>())).ReturnsAsync(new SpendingPlanDto());
|
||||
var spendingPlanService = new SpendingPlanService(spendingPlanRepoMock.Object);
|
||||
|
||||
var result = await spendingPlanService.Create(new());
|
||||
|
||||
spendingPlanRepoMock.Verify(repo => repo.Create(It.IsAny<SpendingPlanDto>()), Times.Once);
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<SpendingPlanViewModel>(result);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task Update_WhenPlanExists_ThenUpdatePlan_ReturnsSpendingPlanViewModel()
|
||||
{
|
||||
var spendingPlanRepoMock = new Mock<ISpendingPlanRepo>();
|
||||
spendingPlanRepoMock.Setup(repo => repo.Update(It.IsAny<SpendingPlanDto>()))
|
||||
.ReturnsAsync(new SpendingPlanDto());
|
||||
var spendingPlanService = new SpendingPlanService(spendingPlanRepoMock.Object);
|
||||
|
||||
var result = await spendingPlanService.Update(new());
|
||||
|
||||
spendingPlanRepoMock.Verify(repo => repo.Update(It.IsAny<SpendingPlanDto>()), Times.Once);
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<SpendingPlanViewModel>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Update_WhenPlanNotFound_ThenThrowsEntityNotFoundException()
|
||||
{
|
||||
var spendingPlanRepoMock = new Mock<ISpendingPlanRepo>();
|
||||
spendingPlanRepoMock.Setup(repo => repo.Update(It.IsAny<SpendingPlanDto>()))
|
||||
.ReturnsAsync((SpendingPlanDto)null);
|
||||
var spendingPlanService = new SpendingPlanService(spendingPlanRepoMock.Object);
|
||||
|
||||
await Assert.ThrowsAsync<EntityNotFoundException>(() => spendingPlanService.Update(new()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WhenPlanExists_ThenDeletePlan_ReturnsSpendingPlanViewModel()
|
||||
{
|
||||
var spendingPlanRepoMock = new Mock<ISpendingPlanRepo>();
|
||||
spendingPlanRepoMock.Setup(repo => repo.Delete(It.IsAny<SpendingPlanSearch>()))
|
||||
.ReturnsAsync(new SpendingPlanDto());
|
||||
var spendingPlanService = new SpendingPlanService(spendingPlanRepoMock.Object);
|
||||
|
||||
var result = await spendingPlanService.Delete(new());
|
||||
|
||||
spendingPlanRepoMock.Verify(repo => repo.Delete(It.IsAny<SpendingPlanSearch>()), Times.Once);
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<SpendingPlanViewModel>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WhenPlanNotFound_ThenThrowsEntityNotFoundException()
|
||||
{
|
||||
var spendingPlanRepoMock = new Mock<ISpendingPlanRepo>();
|
||||
spendingPlanRepoMock.Setup(repo => repo.Delete(It.IsAny<SpendingPlanSearch>()))
|
||||
.ReturnsAsync((SpendingPlanDto)null);
|
||||
var spendingPlanService = new SpendingPlanService(spendingPlanRepoMock.Object);
|
||||
|
||||
await Assert.ThrowsAsync<EntityNotFoundException>(() => spendingPlanService.Delete(new()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetList_ReturnsSpendingPlanViewModels()
|
||||
{
|
||||
var spendingPlanRepoMock = new Mock<ISpendingPlanRepo>();
|
||||
spendingPlanRepoMock.Setup(repo => repo.GetList(It.IsAny<SpendingPlanSearch>()))
|
||||
.ReturnsAsync(_getAllSpendingPlans());
|
||||
var spendingPlanService = new SpendingPlanService(spendingPlanRepoMock.Object);
|
||||
|
||||
var result = await spendingPlanService.GetList(new());
|
||||
|
||||
spendingPlanRepoMock.Verify(repo => repo.GetList(It.IsAny<SpendingPlanSearch>()), Times.Once);
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<List<SpendingPlanViewModel>>(result.ToList());
|
||||
Assert.Equal(2, result.Count());
|
||||
}
|
||||
|
||||
private IEnumerable<SpendingPlanDto> _getAllSpendingPlans() => new List<SpendingPlanDto>()
|
||||
{
|
||||
new() { Id = Guid.NewGuid(), StartAt = DateTime.Now, EndAt = DateTime.Now, Sum = 100 },
|
||||
new() { Id = Guid.NewGuid(), StartAt = DateTime.Now, EndAt = DateTime.Now, Sum = 200 }
|
||||
};
|
||||
}
|
74
back/Services.Tests/Domain/UserServiceTests.cs
Normal file
74
back/Services.Tests/Domain/UserServiceTests.cs
Normal file
@ -0,0 +1,74 @@
|
||||
using Contracts.DTO;
|
||||
using Contracts.Repositories;
|
||||
using Contracts.SearchModels;
|
||||
using Contracts.ViewModels;
|
||||
using Moq;
|
||||
using Services.Domain;
|
||||
using Services.Support.Exceptions;
|
||||
|
||||
namespace Services.Tests.Domain;
|
||||
|
||||
public class UserServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Delete_WhenUserExists_ThenDeleteUser_ReturnsUserViewModel()
|
||||
{
|
||||
var userRepoMock = new Mock<IUserRepo>();
|
||||
userRepoMock.Setup(repo => repo.Delete(It.IsAny<UserSearch>())).ReturnsAsync(new UserDto());
|
||||
var userService = new UserService(userRepoMock.Object);
|
||||
|
||||
var result = await userService.Delete(new UserSearch());
|
||||
|
||||
userRepoMock.Verify(repo => repo.Delete(It.IsAny<UserSearch>()), Times.Once);
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<UserViewModel>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Delete_WhenUserNotExists_ThenThrowsUserNotFoundException()
|
||||
{
|
||||
var userRepoMock = new Mock<IUserRepo>();
|
||||
userRepoMock.Setup(repo => repo.Delete(It.IsAny<UserSearch>())).ReturnsAsync((UserDto?)null);
|
||||
var userService = new UserService(userRepoMock.Object);
|
||||
|
||||
Assert.ThrowsAsync<UserNotFoundException>(() => userService.Delete(new UserSearch()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Update_WhenUserExists_ThenUpdateUser_ReturnsUserViewModel()
|
||||
{
|
||||
var userRepoMock = new Mock<IUserRepo>();
|
||||
userRepoMock.Setup(repo => repo.Update(It.IsAny<UserDto>())).ReturnsAsync(new UserDto());
|
||||
var userService = new UserService(userRepoMock.Object);
|
||||
|
||||
var result = await userService.UpdateUserData(new UserDto());
|
||||
|
||||
userRepoMock.Verify(repo => repo.Update(It.IsAny<UserDto>()), Times.Once);
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<UserViewModel>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WhenUserNotFound_ThenThrowsUserNotFoundException()
|
||||
{
|
||||
var userRepoMock = new Mock<IUserRepo>();
|
||||
userRepoMock.Setup(repo => repo.Update(It.IsAny<UserDto>())).ReturnsAsync((UserDto?)null);
|
||||
var userService = new UserService(userRepoMock.Object);
|
||||
|
||||
Assert.ThrowsAsync<UserNotFoundException>(() => userService.UpdateUserData(new UserDto()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetDetails_ReturnsUserViewModel()
|
||||
{
|
||||
var userRepoMock = new Mock<IUserRepo>();
|
||||
userRepoMock.Setup(repo => repo.Get(It.IsAny<UserSearch>())).ReturnsAsync(new UserDto());
|
||||
var userService = new UserService(userRepoMock.Object);
|
||||
|
||||
var result = await userService.GetDetails(new UserSearch());
|
||||
|
||||
userRepoMock.Verify(repo => repo.Get(It.IsAny<UserSearch>()), Times.Once);
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<UserViewModel>(result);
|
||||
}
|
||||
}
|
28
back/Services.Tests/Services.Tests.csproj
Normal file
28
back/Services.Tests/Services.Tests.csproj
Normal file
@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.5.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Services\Services.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
13
back/Services.Tests/Support/Fakes/Database/DbFake.cs
Normal file
13
back/Services.Tests/Support/Fakes/Database/DbFake.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using Contracts.DTO;
|
||||
|
||||
namespace Services.Tests.Support.Fakes.Database;
|
||||
|
||||
internal static class DbFake
|
||||
{
|
||||
public static List<UserDto> Users = new();
|
||||
|
||||
public static void ClearDb()
|
||||
{
|
||||
Users.Clear();
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
using System.Data.Common;
|
||||
using Contracts.DTO;
|
||||
using Contracts.Repositories;
|
||||
using Contracts.SearchModels;
|
||||
using Services.Tests.Support.Fakes.Database;
|
||||
|
||||
namespace Services.Tests.Support.Fakes.Repositories;
|
||||
|
||||
internal class UserRepoFake : IUserRepo
|
||||
{
|
||||
public async Task<bool> ChangeBalance(UserSearch search, decimal amount)
|
||||
{
|
||||
var user = DbFake.Users.FirstOrDefault(u => u.Id == search.Id);
|
||||
if (user != null)
|
||||
{
|
||||
user.Balance += amount;
|
||||
return await Task.FromResult(true);
|
||||
}
|
||||
return await Task.FromResult(false);
|
||||
}
|
||||
|
||||
public async Task<UserDto> Create(UserDto user)
|
||||
{
|
||||
DbFake.Users.Add(user);
|
||||
return await Task.FromResult(user);
|
||||
}
|
||||
|
||||
public async Task<UserDto?> Delete(UserSearch search)
|
||||
{
|
||||
var user = DbFake.Users.FirstOrDefault(u => u.Id == search.Id);
|
||||
if (user != null)
|
||||
{
|
||||
DbFake.Users.Remove(user);
|
||||
}
|
||||
return await Task.FromResult(user);
|
||||
}
|
||||
|
||||
public async Task<UserDto?> Get(UserSearch search)
|
||||
{
|
||||
return await Task.FromResult(DbFake.Users.FirstOrDefault(u => u.Id == search.Id));
|
||||
}
|
||||
|
||||
public async Task<UserDto?> Update(UserDto user)
|
||||
{
|
||||
var existingUser = DbFake.Users.FirstOrDefault(u => u.Id == user.Id);
|
||||
if (existingUser != null)
|
||||
{
|
||||
existingUser.Name = user.Name;
|
||||
existingUser.Password = user.Password;
|
||||
DbFake.Users.Remove(existingUser);
|
||||
DbFake.Users.Add(existingUser);
|
||||
}
|
||||
return await Task.FromResult(existingUser);
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@ public class AuthService : IAuthService
|
||||
_userRepo = userRepo;
|
||||
}
|
||||
|
||||
public async Task<UserViewModel> Login(UserLoginDTO loginData)
|
||||
public async Task<UserViewModel> Login(UserLoginDto loginData)
|
||||
{
|
||||
if (loginData == null || string.IsNullOrWhiteSpace(loginData.Name)
|
||||
|| string.IsNullOrWhiteSpace(loginData.Password))
|
||||
|
@ -4,6 +4,7 @@ using Contracts.Repositories;
|
||||
using Contracts.SearchModels;
|
||||
using Contracts.Services;
|
||||
using Contracts.ViewModels;
|
||||
using Services.Support.Exceptions;
|
||||
|
||||
namespace Services.Domain;
|
||||
|
||||
@ -32,7 +33,7 @@ public class ChangeRecordService : IChangeRecordService
|
||||
var record = await _changeRecordRepo.Delete(search);
|
||||
if (record == null)
|
||||
{
|
||||
throw new EntryPointNotFoundException("При удалении не получилось найти запись измнения баланса");
|
||||
throw new EntityNotFoundException("При удалении не получилось найти запись измнения баланса");
|
||||
}
|
||||
// Возвращает баланс обратно
|
||||
await _userRepo.ChangeBalance(new() { Id = record.UserId }, -record.Sum);
|
||||
@ -51,7 +52,7 @@ public class ChangeRecordService : IChangeRecordService
|
||||
var record = await _changeRecordRepo.Update(model);
|
||||
if (record == null)
|
||||
{
|
||||
throw new EntryPointNotFoundException("При изменении не получилось найти запись измнения баланса");
|
||||
throw new EntityNotFoundException("При изменении не получилось найти запись измнения баланса");
|
||||
}
|
||||
await _userRepo.ChangeBalance(new() { Id = model.UserId }, model.Sum - record.Sum);
|
||||
return record.ToView();
|
||||
|
41
docker-compose.debug.yml
Normal file
41
docker-compose.debug.yml
Normal file
@ -0,0 +1,41 @@
|
||||
# Please refer https://aka.ms/HTTPSinContainer on how to setup an https developer certificate for your ASP.NET Core service.
|
||||
services:
|
||||
api:
|
||||
image: api
|
||||
build:
|
||||
context: .
|
||||
dockerfile: back/Controllers/Dockerfile
|
||||
args:
|
||||
- configuration=Debug
|
||||
ports:
|
||||
- 5125:5125
|
||||
environment:
|
||||
- ConnectionStrings__DefaultConnection=${CONNECTION_STRING}
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
volumes:
|
||||
- ~/.vsdbg:/remote_debugger:rw
|
||||
depends_on:
|
||||
- database
|
||||
dombudg:
|
||||
image: dombudg
|
||||
build:
|
||||
context: front
|
||||
dockerfile: ./Dockerfile
|
||||
environment:
|
||||
- VITE_API_URL=http://api:5125
|
||||
ports:
|
||||
- 80:80
|
||||
depends_on:
|
||||
- api
|
||||
database:
|
||||
image: postgres:14
|
||||
environment:
|
||||
- POSTGRES_DB=${POSTGRES_DB}
|
||||
- POSTGRES_USER=${POSTGRES_USER}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
35
docker-compose.yml
Normal file
35
docker-compose.yml
Normal file
@ -0,0 +1,35 @@
|
||||
services:
|
||||
api:
|
||||
image: api
|
||||
build:
|
||||
context: .
|
||||
dockerfile: back/Controllers/Dockerfile
|
||||
ports:
|
||||
- 5125:5125
|
||||
environment:
|
||||
- ConnectionStrings__DefaultConnection=${CONNECTION_STRING}
|
||||
depends_on:
|
||||
- database
|
||||
dombudg:
|
||||
image: dombudg
|
||||
build:
|
||||
context: front
|
||||
dockerfile: ./Dockerfile
|
||||
environment:
|
||||
- VITE_API_URL=http://api:5125
|
||||
ports:
|
||||
- 80:80
|
||||
depends_on:
|
||||
- api
|
||||
database:
|
||||
image: postgres:14
|
||||
environment:
|
||||
- POSTGRES_DB=${POSTGRES_DB}
|
||||
- POSTGRES_USER=${POSTGRES_USER}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
24
front/.dockerignore
Normal file
24
front/.dockerignore
Normal file
@ -0,0 +1,24 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
2
front/.gitignore
vendored
2
front/.gitignore
vendored
@ -22,3 +22,5 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.env
|
18
front/Dockerfile
Normal file
18
front/Dockerfile
Normal file
@ -0,0 +1,18 @@
|
||||
FROM node as vite-app
|
||||
|
||||
ARG VITE_API_URL
|
||||
|
||||
WORKDIR /app/client
|
||||
COPY . .
|
||||
|
||||
RUN ["npm", "i"]
|
||||
RUN ["npm", "run", "build"]
|
||||
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY nginx.conf /etc/nginx
|
||||
|
||||
RUN rm -rf /usr/share/nginx/html/*
|
||||
COPY --from=vite-app /app/client/dist /usr/share/nginx/html
|
||||
|
||||
ENTRYPOINT ["nginx", "-g", "daemon off;"]
|
38
front/components.d.ts
vendored
Normal file
38
front/components.d.ts
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AButton: typeof import('ant-design-vue/es')['Button']
|
||||
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
|
||||
AForm: typeof import('ant-design-vue/es')['Form']
|
||||
AFormItem: typeof import('ant-design-vue/es')['FormItem']
|
||||
AInput: typeof import('ant-design-vue/es')['Input']
|
||||
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
|
||||
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
||||
ALayout: typeof import('ant-design-vue/es')['Layout']
|
||||
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
|
||||
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
|
||||
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
|
||||
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']
|
||||
ChangeRecordManager: typeof import('./src/components/support/ChangeRecordManager.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']
|
||||
PlanManager: typeof import('./src/components/support/PlanManager.vue')['default']
|
||||
Plans: typeof import('./src/components/pages/Plans.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SignUp: typeof import('./src/components/pages/SignUp.vue')['default']
|
||||
SpendingGroupManager: typeof import('./src/components/support/SpendingGroupManager.vue')['default']
|
||||
}
|
||||
}
|
55
front/nginx.conf
Normal file
55
front/nginx.conf
Normal file
@ -0,0 +1,55 @@
|
||||
# Запускать в качестве менее привилегированного пользователя по соображениям безопасности..
|
||||
user nginx;
|
||||
|
||||
# Значение auto устанавливает число максимально доступных ядер CPU,
|
||||
# чтобы обеспечить лучшую производительность.
|
||||
worker_processes auto;
|
||||
|
||||
events { worker_connections 1024; }
|
||||
|
||||
http {
|
||||
server {
|
||||
# Hide nginx version information.
|
||||
server_tokens off;
|
||||
|
||||
listen 80;
|
||||
root /usr/share/nginx/html;
|
||||
include /etc/nginx/mime.types;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://api:5125/api/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Prefix /test;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_http_version 1.0;
|
||||
gzip_comp_level 5;
|
||||
gzip_types
|
||||
application/atom+xml
|
||||
application/javascript
|
||||
application/json
|
||||
application/rss+xml
|
||||
application/vnd.ms-fontobject
|
||||
application/x-font-ttf
|
||||
application/x-web-app-manifest+json
|
||||
application/xhtml+xml
|
||||
application/xml
|
||||
font/opentype
|
||||
image/svg+xml
|
||||
image/x-icon
|
||||
text/css
|
||||
text/plain
|
||||
text/x-component;
|
||||
gzip_proxied no-cache no-store private expired auth;
|
||||
gzip_min_length 256;
|
||||
gunzip on;
|
||||
}
|
||||
}
|
1716
front/package-lock.json
generated
1716
front/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,14 +6,22 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"gen-api": "swagger-typescript-api -r -o ./src/core/api/ --modular -p "
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.12"
|
||||
"@vueuse/core": "^12.0.0",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"dayjs": "^1.11.13",
|
||||
"pinia": "^2.2.8",
|
||||
"vue": "^3.5.12",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"swagger-typescript-api": "^13.0.23",
|
||||
"typescript": "~5.6.2",
|
||||
"unplugin-vue-components": "^0.27.5",
|
||||
"vite": "^5.4.10",
|
||||
"vue-tsc": "^2.1.8"
|
||||
}
|
||||
|
@ -1,9 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import Header from './components/main/Header.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<a-layout class="layout">
|
||||
<Header />
|
||||
<a-layout-content>
|
||||
<RouterView />
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
main {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 5vh;
|
||||
}
|
||||
|
||||
.base-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 80dvw;
|
||||
}
|
||||
</style>
|
59
front/src/components/main/Header.vue
Normal file
59
front/src/components/main/Header.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue';
|
||||
import { useUserStore } from '../../store';
|
||||
import { AuthService } from '../../core/services/auth-service';
|
||||
import router from '../../router';
|
||||
|
||||
const store = useUserStore();
|
||||
const authService = inject(AuthService.name) as AuthService;
|
||||
|
||||
function logout() {
|
||||
authService.logout();
|
||||
router.push({ name: 'login' });
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-layout-header class="header">
|
||||
<div class="base-nav">
|
||||
<div>ДомБюдж</div>
|
||||
<nav>
|
||||
<RouterLink :to="{ name: 'home' }">Главная</RouterLink>
|
||||
<RouterLink :to="{ name: 'groups' }">Группы расходов</RouterLink>
|
||||
</nav>
|
||||
</div>
|
||||
<div v-if="!store.user.id">
|
||||
<RouterLink :to="{ name: 'login' }">Войти</RouterLink>
|
||||
</div>
|
||||
<div v-else>
|
||||
<label for="logout">Привет, {{ store.user.name }}! Ваш текущий баланс: {{ store.user.balance }}</label>
|
||||
<a-button
|
||||
name="logout"
|
||||
@click="logout()"
|
||||
danger
|
||||
style="margin-left: 30px"
|
||||
>
|
||||
Выйти
|
||||
</a-button>
|
||||
</div>
|
||||
</a-layout-header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
background-color: white;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.base-nav {
|
||||
display: inline-flex;
|
||||
justify-content: left;
|
||||
}
|
||||
.base-nav a {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
</style>
|
72
front/src/components/pages/Groups.vue
Normal file
72
front/src/components/pages/Groups.vue
Normal file
@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
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';
|
||||
|
||||
const groupService = inject(GroupService.name) as GroupService;
|
||||
|
||||
const { state, isReady } = useAsyncState(() => groupService.getList(), []);
|
||||
const columns = [
|
||||
{
|
||||
title: "Название группы",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
},
|
||||
{
|
||||
title: "Планы группы",
|
||||
dataIndex: "plans",
|
||||
key: "plans",
|
||||
},
|
||||
{
|
||||
title: 'Операция',
|
||||
dataIndex: 'operation',
|
||||
key: 'operation',
|
||||
}
|
||||
]
|
||||
|
||||
const refreshData = () => {
|
||||
groupService.getList().then(data => {
|
||||
state.value = data;
|
||||
isReady.value = true;
|
||||
});
|
||||
}
|
||||
|
||||
const onDelete = (key: string) => {
|
||||
groupService.deleteGroup(key)
|
||||
.then(() => {
|
||||
refreshData();
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="base-page">
|
||||
<h1>Группы расходов</h1>
|
||||
<SpendingGroupManager :refreshData="refreshData" />
|
||||
<a-table :dataSource="state" :columns="columns" v-if="isReady">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'plans'">
|
||||
<RouterLink :to="{ name: 'plans', params: { groupId: record.id } }" >Планы</RouterLink>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'operation'">
|
||||
<a-popconfirm
|
||||
v-if="state?.length"
|
||||
title="Точно удалить?"
|
||||
@confirm="onDelete(record.id)"
|
||||
>
|
||||
<a><DeleteOutlined /> Удалить</a>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<div v-else>
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
76
front/src/components/pages/Home.vue
Normal file
76
front/src/components/pages/Home.vue
Normal file
@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import { useAsyncState } from '@vueuse/core';
|
||||
import ChangeRecordMenu from '../support/ChangeRecordManager.vue';
|
||||
import { ChangeRecordService } from '../../core/services/change-record-service';
|
||||
import { inject } from 'vue';
|
||||
import { DeleteOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
const changeRecordService = inject(ChangeRecordService.name) as ChangeRecordService;
|
||||
|
||||
const { state, isReady } = useAsyncState(() => changeRecordService.getList(), []);
|
||||
const columns = [
|
||||
{
|
||||
title: 'Дата',
|
||||
dataIndex: 'changedAt',
|
||||
key: 'changedAt',
|
||||
},
|
||||
{
|
||||
title: 'Сумма',
|
||||
dataIndex: 'sum',
|
||||
key: 'sum',
|
||||
},
|
||||
{
|
||||
title: 'Группа расходов',
|
||||
dataIndex: 'spendingGroupName',
|
||||
key: 'spendingGroupName',
|
||||
},
|
||||
{
|
||||
title: 'Операция',
|
||||
dataIndex: 'operation',
|
||||
key: 'operation',
|
||||
}
|
||||
]
|
||||
|
||||
const refreshData = () => {
|
||||
changeRecordService.getList().then(data => {
|
||||
state.value = data;
|
||||
isReady.value = true;
|
||||
});
|
||||
}
|
||||
|
||||
const onDelete = (key: string) => {
|
||||
changeRecordService.deleteRecord(key)
|
||||
.then(() => {
|
||||
refreshData();
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="base-page">
|
||||
<h1>История изменений баланса</h1>
|
||||
<ChangeRecordMenu :refreshData="refreshData" />
|
||||
<a-table :dataSource="state" :columns="columns" v-if="isReady" >
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'operation'">
|
||||
<a-popconfirm
|
||||
v-if="state?.length"
|
||||
title="Точно удалить?"
|
||||
@confirm="onDelete(record.id)"
|
||||
>
|
||||
<a><DeleteOutlined /> Удалить</a>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<div v-else>
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.layout {
|
||||
|
||||
}
|
||||
</style>
|
76
front/src/components/pages/Login.vue
Normal file
76
front/src/components/pages/Login.vue
Normal file
@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<a-form
|
||||
:model="formState"
|
||||
name="login"
|
||||
class="login-form"
|
||||
@finish="onFinish"
|
||||
@finishFailed="onFinishFailed"
|
||||
>
|
||||
<a-form-item
|
||||
label="Логин"
|
||||
name="name"
|
||||
:rules="[{ required: true, message: 'Пожалуйста, введите свой логин' }]"
|
||||
>
|
||||
<a-input v-model:value="formState.name">
|
||||
<template #prefix>
|
||||
<UserOutlined class="site-form-item-icon" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="Пароль"
|
||||
name="password"
|
||||
:rules="[{ required: true, message: 'Пароль тоже нужен!' }]"
|
||||
>
|
||||
<a-input-password v-model:value="formState.password">
|
||||
<template #prefix>
|
||||
<LockOutlined class="site-form-item-icon" />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button :disabled="disabled" type="primary" html-type="submit" class="login-form-button">
|
||||
Войти
|
||||
</a-button>
|
||||
Или
|
||||
<RouterLink :to="{ name: 'signup' }">создать аккаунт</RouterLink>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, computed, inject } from 'vue';
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue';
|
||||
import { UserLoginDto } from '../../core/api/data-contracts';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import { AuthService } from '../../core/services/auth-service';
|
||||
import router from '../../router';
|
||||
|
||||
const formState = reactive<UserLoginDto>({
|
||||
name: '',
|
||||
password: '',
|
||||
});
|
||||
const authService = inject(AuthService.name) as AuthService;
|
||||
console.log(authService);
|
||||
// Логика формы
|
||||
const onFinish = async (values: any) => {
|
||||
console.log('Success:', values);
|
||||
await authService.login(formState);
|
||||
router.push({ name: 'home' });
|
||||
};
|
||||
const onFinishFailed = (errorInfo: any) => {
|
||||
console.log('Failed:', errorInfo);
|
||||
};
|
||||
const disabled = computed(() => {
|
||||
return !(formState.name && formState.password);
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.login-form {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.login-form-button {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
79
front/src/components/pages/Plans.vue
Normal file
79
front/src/components/pages/Plans.vue
Normal file
@ -0,0 +1,79 @@
|
||||
<script setup lang="ts">
|
||||
import { useAsyncState } from '@vueuse/core';
|
||||
import { inject } from 'vue';
|
||||
import { PlanService } from '../../core/services/plans-service';
|
||||
import { useRoute } from 'vue-router';
|
||||
import PlanManager from '../support/PlanManager.vue';
|
||||
import { DeleteOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
const planService = inject(PlanService.name) as PlanService;
|
||||
const groupId = useRoute().params.groupId as string;
|
||||
|
||||
|
||||
|
||||
const { state, isReady } = useAsyncState(() => planService.getList(groupId), []);
|
||||
const columns = [
|
||||
{
|
||||
title: "Планируемые расходы",
|
||||
dataIndex: "sum",
|
||||
key: "sum",
|
||||
},
|
||||
{
|
||||
title: "Начало плана",
|
||||
dataIndex: "startAt",
|
||||
key: "startAt",
|
||||
},
|
||||
{
|
||||
title: "Конец плана",
|
||||
dataIndex: "endAt",
|
||||
key: "endAt",
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Операция',
|
||||
dataIndex: 'operation',
|
||||
key: 'operation',
|
||||
}
|
||||
]
|
||||
|
||||
const refreshData = () => {
|
||||
planService.getList(groupId).then(data => {
|
||||
state.value = data;
|
||||
isReady.value = true;
|
||||
});
|
||||
};
|
||||
|
||||
const onDelete = (key: string) => {
|
||||
planService.deletePlan(key)
|
||||
.then(() => {
|
||||
refreshData();
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="base-page">
|
||||
<h1>Планы группы</h1>
|
||||
<PlanManager :groupId="groupId" :refreshData="refreshData"/>
|
||||
<a-table :dataSource="state" :columns="columns" v-if="isReady">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'operation'">
|
||||
<a-popconfirm
|
||||
v-if="state?.length"
|
||||
title="Точно удалить?"
|
||||
@confirm="onDelete(record.id)"
|
||||
>
|
||||
<a><DeleteOutlined /> Удалить</a>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<div v-else>
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
86
front/src/components/pages/SignUp.vue
Normal file
86
front/src/components/pages/SignUp.vue
Normal file
@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<a-form
|
||||
:model="formState"
|
||||
name="signup"
|
||||
class="signup-form"
|
||||
@finish="onFinish"
|
||||
@finishFailed="onFinishFailed"
|
||||
>
|
||||
<a-form-item
|
||||
label="Логин"
|
||||
name="name"
|
||||
:rules="[{ required: true, message: 'Пожалуйста, введите свой логин' }]"
|
||||
>
|
||||
<a-input v-model:value="formState.name">
|
||||
<template #prefix>
|
||||
<UserOutlined class="site-form-item-icon" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="Пароль"
|
||||
name="password"
|
||||
:rules="[{ required: true, message: 'Пароль тоже нужен!' }]"
|
||||
>
|
||||
<a-input-password v-model:value="formState.password">
|
||||
<template #prefix>
|
||||
<LockOutlined class="site-form-item-icon" />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="Баланс"
|
||||
name="balance"
|
||||
:rules="[{ required: true, message: 'Пожалуйста, введите свой баланс' }]"
|
||||
>
|
||||
<a-input-number prefix="₽" v-model:value="formState.balance" min="0"/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button :disabled="disabled" type="primary" html-type="submit" class="signup-form-button">
|
||||
Создать
|
||||
</a-button>
|
||||
Или
|
||||
<RouterLink :to="{ name: 'login' }">войти в свой аккаунт</RouterLink>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, computed, inject } from 'vue';
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue';
|
||||
import { UserDto } from '../../core/api/data-contracts';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import { AuthService } from '../../core/services/auth-service';
|
||||
import router from '../../router';
|
||||
|
||||
const formState = reactive<UserDto>({
|
||||
name: '',
|
||||
password: '',
|
||||
balance: 0
|
||||
});
|
||||
const authService = inject(AuthService.name) as AuthService;
|
||||
|
||||
// Логика формы
|
||||
const onFinish = async (values: any) => {
|
||||
console.log('Success:', values);
|
||||
await authService.register(formState);
|
||||
router.push({ name: 'home' });
|
||||
};
|
||||
const onFinishFailed = (errorInfo: any) => {
|
||||
console.log('Failed:', errorInfo);
|
||||
};
|
||||
const disabled = computed(() => {
|
||||
return !(formState.name && formState.password && formState.balance);
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.signup-form {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.signup-form-button {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
95
front/src/components/support/ChangeRecordManager.vue
Normal file
95
front/src/components/support/ChangeRecordManager.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<style scoped>
|
||||
</style>
|
||||
<template>
|
||||
<a-space direction="vertical" :size="10">
|
||||
<label for="change-record">Записать изменения баланса</label>
|
||||
<a-form
|
||||
:model="formState"
|
||||
name="change-record"
|
||||
@finish="onFinish"
|
||||
@finishFailed="onFinishFailed"
|
||||
>
|
||||
<a-form-item
|
||||
label="Сумма"
|
||||
name="sum"
|
||||
:rules="[{ required: true, message: 'Пожалуйста, введите сумму' }]"
|
||||
>
|
||||
<a-input-number v-model:value="formState.sum" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="Группа расходов"
|
||||
name="spendingGroupId"
|
||||
v-if="isReady"
|
||||
:rules="[{ required: true, message: 'Пожалуйста, выберите группу расходов' }]"
|
||||
>
|
||||
|
||||
<a-select v-model:value="formState.spendingGroupId" :disabled="disabled">
|
||||
<a-select-option v-for="group in groupList" :key="group.id" :value="group.id">
|
||||
{{ group.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-else>
|
||||
<a-spin size="small" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
name="changedAt"
|
||||
label="Дата"
|
||||
:rules="[{ required: true, message: 'Пожалуйста, выберите дату' }]"
|
||||
>
|
||||
<a-date-picker v-model:value="pickedChangedAt" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button html-type="submit" :disabled="disabledForm" type="primary">Сохранить</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-space>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, reactive, ref } from 'vue';
|
||||
import { ChangeRecordDto } from '../../core/api/data-contracts';
|
||||
import { useUserStore } from '../../store';
|
||||
import { ChangeRecordService } from '../../core/services/change-record-service';
|
||||
import { GroupService } from '../../core/services/group-service';
|
||||
import { useAsyncState } from '@vueuse/core';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
interface IProps {
|
||||
refreshData: () => void
|
||||
}
|
||||
|
||||
const { refreshData } = defineProps<IProps>();
|
||||
|
||||
const store = useUserStore();
|
||||
// Сервисы
|
||||
const changeRecordService = inject(ChangeRecordService.name) as ChangeRecordService;
|
||||
const groupService = inject(GroupService.name) as GroupService;
|
||||
|
||||
const { state: groupList, isReady } = useAsyncState(() => groupService.getList(), []);
|
||||
const pickedChangedAt = ref<Dayjs>();
|
||||
const formState = reactive<ChangeRecordDto>({
|
||||
userId: store.user.id,
|
||||
sum: -1,
|
||||
changedAt: new Date().toISOString(),
|
||||
spendingGroupId: null
|
||||
});
|
||||
|
||||
// Логика формы
|
||||
const onFinish = async (values: any) => {
|
||||
console.log('Success:', values);
|
||||
formState.changedAt = pickedChangedAt.value?.toISOString();
|
||||
await changeRecordService.createRecord(formState);
|
||||
refreshData();
|
||||
};
|
||||
const onFinishFailed = (errorInfo: any) => {
|
||||
console.log('Failed:', errorInfo);
|
||||
};
|
||||
const disabled = computed(() => {
|
||||
return formState.sum && formState.sum >= 0;
|
||||
});
|
||||
const disabledForm = computed(() => {
|
||||
return !(formState.sum && formState.changedAt && (formState.spendingGroupId || formState.sum > 0));
|
||||
})
|
||||
</script>
|
80
front/src/components/support/PlanManager.vue
Normal file
80
front/src/components/support/PlanManager.vue
Normal file
@ -0,0 +1,80 @@
|
||||
<style scoped>
|
||||
</style>
|
||||
<template>
|
||||
<a-space direction="vertical" :size="10">
|
||||
<label for="change-record">Добавить план расходов</label>
|
||||
<a-form
|
||||
:model="formState"
|
||||
name="change-record"
|
||||
@finish="onFinish"
|
||||
@finishFailed="onFinishFailed"
|
||||
>
|
||||
<a-form-item
|
||||
label="Сумма"
|
||||
name="sum"
|
||||
:rules="[{ required: true, message: 'Пожалуйста, введите сумму' }]"
|
||||
>
|
||||
<a-input-number v-model:value="formState.sum" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
name="startAt"
|
||||
label="Дата начала плана"
|
||||
:rules="[{ required: true, message: 'Пожалуйста, введите дату начала плана' }]"
|
||||
>
|
||||
<a-date-picker v-model:value="startAt" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
name="endAt"
|
||||
label="Дата окончания плана"
|
||||
:rules="[{ required: true, message: 'Пожалуйста, введите дату окончания плана' }]"
|
||||
>
|
||||
<a-date-picker v-model:value="endAt" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button :disabled="disabledForm" html-type="submit" type="primary">Добавить</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-space>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, reactive, ref } from 'vue';
|
||||
import { SpendingPlanDto } from '../../core/api/data-contracts';
|
||||
import { Dayjs } from 'dayjs';
|
||||
import { PlanService } from '../../core/services/plans-service';
|
||||
|
||||
interface IProps {
|
||||
groupId: string,
|
||||
refreshData: () => void
|
||||
}
|
||||
const { groupId, refreshData } = defineProps<IProps>();
|
||||
// Сервисы
|
||||
const planService = inject(PlanService.name) as PlanService;
|
||||
|
||||
const startAt = ref<Dayjs>();
|
||||
const endAt = ref<Dayjs>();
|
||||
|
||||
const formState = reactive<SpendingPlanDto>({
|
||||
sum: 0,
|
||||
startAt: new Date().toISOString(),
|
||||
endAt: new Date().toISOString(),
|
||||
spendingGroupId: groupId
|
||||
});
|
||||
|
||||
// Логика формы
|
||||
const onFinish = async (values: any) => {
|
||||
console.log('Success:', values);
|
||||
formState.startAt = startAt.value?.toISOString();
|
||||
formState.endAt = endAt.value?.toISOString();
|
||||
await planService.createPlan(formState);
|
||||
refreshData();
|
||||
};
|
||||
const onFinishFailed = (errorInfo: any) => {
|
||||
console.log('Failed:', errorInfo);
|
||||
};
|
||||
|
||||
const disabledForm = computed(() => {
|
||||
return !(formState.sum && formState.startAt && formState.endAt);
|
||||
});
|
||||
</script>
|
57
front/src/components/support/SpendingGroupManager.vue
Normal file
57
front/src/components/support/SpendingGroupManager.vue
Normal file
@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<a-space direction="vertical" :size="10">
|
||||
<label for="spending-group">Создать группу расходов</label>
|
||||
<a-form
|
||||
:model="formState"
|
||||
name="spending-group"
|
||||
@finish="onFinish"
|
||||
@finishFailed="onFinishFailed"
|
||||
>
|
||||
<a-form-item
|
||||
label="Название"
|
||||
name="name"
|
||||
:rules="[{ required: true, message: 'Пожалуйста, введите название' }]"
|
||||
>
|
||||
<a-input v-model:value="formState.name" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button :disabled="disabledForm" html-type="submit" type="primary">Сохранить</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-space>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, reactive } from 'vue';
|
||||
import { useUserStore } from '../../store';
|
||||
import { GroupService } from '../../core/services/group-service';
|
||||
import { SpendingGroupDto } from '../../core/api/data-contracts';
|
||||
|
||||
interface IProps {
|
||||
refreshData: () => void
|
||||
}
|
||||
|
||||
const { refreshData } = defineProps<IProps>();
|
||||
|
||||
const store = useUserStore();
|
||||
// Сервисы
|
||||
const groupService = inject(GroupService.name) as GroupService;
|
||||
|
||||
const formState = reactive<SpendingGroupDto>({
|
||||
userId: store.user.id,
|
||||
name: '',
|
||||
});
|
||||
|
||||
// Логика формы
|
||||
const onFinish = async (values: any) => {
|
||||
console.log('Success:', values);
|
||||
await groupService.createGroup(formState);
|
||||
refreshData();
|
||||
};
|
||||
const onFinishFailed = (errorInfo: any) => {
|
||||
console.log('Failed:', errorInfo);
|
||||
};
|
||||
|
||||
const disabledForm = computed(() => {
|
||||
return !formState.name;
|
||||
});
|
||||
</script>
|
470
front/src/core/api/Api.ts
Normal file
470
front/src/core/api/Api.ts
Normal file
@ -0,0 +1,470 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
/*
|
||||
* ---------------------------------------------------------------
|
||||
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||
* ## ##
|
||||
* ## AUTHOR: acacode ##
|
||||
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import {
|
||||
ChangeRecordDto,
|
||||
ChangeRecordViewModel,
|
||||
SpendingGroupDto,
|
||||
SpendingGroupViewModel,
|
||||
SpendingPlanDto,
|
||||
SpendingPlanViewModel,
|
||||
UserDto,
|
||||
UserLoginDto,
|
||||
UserViewModel,
|
||||
} from "./data-contracts";
|
||||
import { ContentType, HttpClient, RequestParams } from "./http-client";
|
||||
|
||||
export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType> {
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags Auth
|
||||
* @name AuthCreate
|
||||
* @request POST:/api/Auth
|
||||
* @response `200` `UserViewModel` Success
|
||||
*/
|
||||
auth = (data: UserLoginDto, params: RequestParams = {}) =>
|
||||
this.request<UserViewModel, any>({
|
||||
path: `/api/Auth`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags Auth
|
||||
* @name AuthRegisterCreate
|
||||
* @request POST:/api/Auth/register
|
||||
* @response `200` `UserViewModel` Success
|
||||
*/
|
||||
authRegister = (data: UserDto, params: RequestParams = {}) =>
|
||||
this.request<UserViewModel, any>({
|
||||
path: `/api/Auth/register`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags ChangeRecord
|
||||
* @name ChangeRecordCreate
|
||||
* @request POST:/api/ChangeRecord
|
||||
* @response `200` `ChangeRecordViewModel` Success
|
||||
*/
|
||||
changeRecordCreate = (data: ChangeRecordDto, params: RequestParams = {}) =>
|
||||
this.request<ChangeRecordViewModel, any>({
|
||||
path: `/api/ChangeRecord`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags ChangeRecord
|
||||
* @name ChangeRecordList
|
||||
* @request GET:/api/ChangeRecord
|
||||
* @response `200` `(ChangeRecordViewModel)[]` Success
|
||||
*/
|
||||
changeRecordList = (params: RequestParams = {}) =>
|
||||
this.request<ChangeRecordViewModel[], any>({
|
||||
path: `/api/ChangeRecord`,
|
||||
method: "GET",
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags ChangeRecord
|
||||
* @name ChangeRecordPartialUpdate
|
||||
* @request PATCH:/api/ChangeRecord
|
||||
* @response `200` `ChangeRecordViewModel` Success
|
||||
*/
|
||||
changeRecordPartialUpdate = (data: ChangeRecordDto, params: RequestParams = {}) =>
|
||||
this.request<ChangeRecordViewModel, any>({
|
||||
path: `/api/ChangeRecord`,
|
||||
method: "PATCH",
|
||||
body: data,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags ChangeRecord
|
||||
* @name ChangeRecordDelete
|
||||
* @request DELETE:/api/ChangeRecord
|
||||
* @response `200` `void` Success
|
||||
*/
|
||||
changeRecordDelete = (
|
||||
query?: {
|
||||
/** @format uuid */
|
||||
Id?: string;
|
||||
/** @format uuid */
|
||||
SpendingGroupId?: string;
|
||||
/** @format date-time */
|
||||
From?: string;
|
||||
/** @format date-time */
|
||||
To?: string;
|
||||
/** @format uuid */
|
||||
UserId?: string;
|
||||
},
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/ChangeRecord`,
|
||||
method: "DELETE",
|
||||
query: query,
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags ChangeRecord
|
||||
* @name ChangeRecordFilterList
|
||||
* @request GET:/api/ChangeRecord/filter
|
||||
* @response `200` `(ChangeRecordViewModel)[]` Success
|
||||
*/
|
||||
changeRecordFilterList = (
|
||||
query?: {
|
||||
/** @format uuid */
|
||||
Id?: string;
|
||||
/** @format uuid */
|
||||
SpendingGroupId?: string;
|
||||
/** @format date-time */
|
||||
From?: string;
|
||||
/** @format date-time */
|
||||
To?: string;
|
||||
/** @format uuid */
|
||||
UserId?: string;
|
||||
},
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
this.request<ChangeRecordViewModel[], any>({
|
||||
path: `/api/ChangeRecord/filter`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags SpendingGroup
|
||||
* @name SpendingGroupDetail
|
||||
* @request GET:/api/SpendingGroup/{id}
|
||||
* @response `200` `SpendingGroupViewModel` Success
|
||||
*/
|
||||
spendingGroupDetail = (
|
||||
id: string,
|
||||
query?: {
|
||||
/** @format uuid */
|
||||
Id?: string;
|
||||
Name?: string;
|
||||
/** @format uuid */
|
||||
UserId?: string;
|
||||
},
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
this.request<SpendingGroupViewModel, any>({
|
||||
path: `/api/SpendingGroup/${id}`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags SpendingGroup
|
||||
* @name SpendingGroupList
|
||||
* @request GET:/api/SpendingGroup
|
||||
* @response `200` `(SpendingGroupViewModel)[]` Success
|
||||
*/
|
||||
spendingGroupList = (params: RequestParams = {}) =>
|
||||
this.request<SpendingGroupViewModel[], any>({
|
||||
path: `/api/SpendingGroup`,
|
||||
method: "GET",
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags SpendingGroup
|
||||
* @name SpendingGroupCreate
|
||||
* @request POST:/api/SpendingGroup
|
||||
* @response `200` `SpendingGroupViewModel` Success
|
||||
*/
|
||||
spendingGroupCreate = (data: SpendingGroupDto, params: RequestParams = {}) =>
|
||||
this.request<SpendingGroupViewModel, any>({
|
||||
path: `/api/SpendingGroup`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags SpendingGroup
|
||||
* @name SpendingGroupPartialUpdate
|
||||
* @request PATCH:/api/SpendingGroup
|
||||
* @response `200` `SpendingGroupViewModel` Success
|
||||
*/
|
||||
spendingGroupPartialUpdate = (data: SpendingGroupDto, params: RequestParams = {}) =>
|
||||
this.request<SpendingGroupViewModel, any>({
|
||||
path: `/api/SpendingGroup`,
|
||||
method: "PATCH",
|
||||
body: data,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags SpendingGroup
|
||||
* @name SpendingGroupDelete
|
||||
* @request DELETE:/api/SpendingGroup
|
||||
* @response `200` `void` Success
|
||||
*/
|
||||
spendingGroupDelete = (
|
||||
query?: {
|
||||
/** @format uuid */
|
||||
Id?: string;
|
||||
Name?: string;
|
||||
/** @format uuid */
|
||||
UserId?: string;
|
||||
},
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/SpendingGroup`,
|
||||
method: "DELETE",
|
||||
query: query,
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags SpendingGroup
|
||||
* @name SpendingGroupFilterList
|
||||
* @request GET:/api/SpendingGroup/filter
|
||||
* @response `200` `(SpendingGroupViewModel)[]` Success
|
||||
*/
|
||||
spendingGroupFilterList = (
|
||||
query?: {
|
||||
/** @format uuid */
|
||||
Id?: string;
|
||||
Name?: string;
|
||||
/** @format uuid */
|
||||
UserId?: string;
|
||||
},
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
this.request<SpendingGroupViewModel[], any>({
|
||||
path: `/api/SpendingGroup/filter`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags SpendingPlan
|
||||
* @name SpendingPlanDetail
|
||||
* @request GET:/api/SpendingPlan/{id}
|
||||
* @response `200` `SpendingPlanViewModel` Success
|
||||
*/
|
||||
spendingPlanDetail = (
|
||||
id: string,
|
||||
query?: {
|
||||
/** @format uuid */
|
||||
Id?: string;
|
||||
},
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
this.request<SpendingPlanViewModel, any>({
|
||||
path: `/api/SpendingPlan/${id}`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags SpendingPlan
|
||||
* @name SpendingPlanList
|
||||
* @request GET:/api/SpendingPlan
|
||||
* @response `200` `(SpendingPlanViewModel)[]` Success
|
||||
*/
|
||||
spendingPlanList = (params: RequestParams = {}) =>
|
||||
this.request<SpendingPlanViewModel[], any>({
|
||||
path: `/api/SpendingPlan`,
|
||||
method: "GET",
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags SpendingPlan
|
||||
* @name SpendingPlanCreate
|
||||
* @request POST:/api/SpendingPlan
|
||||
* @response `200` `SpendingPlanViewModel` Success
|
||||
*/
|
||||
spendingPlanCreate = (data: SpendingPlanDto, params: RequestParams = {}) =>
|
||||
this.request<SpendingPlanViewModel, any>({
|
||||
path: `/api/SpendingPlan`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags SpendingPlan
|
||||
* @name SpendingPlanPartialUpdate
|
||||
* @request PATCH:/api/SpendingPlan
|
||||
* @response `200` `SpendingPlanViewModel` Success
|
||||
*/
|
||||
spendingPlanPartialUpdate = (data: SpendingPlanDto, params: RequestParams = {}) =>
|
||||
this.request<SpendingPlanViewModel, any>({
|
||||
path: `/api/SpendingPlan`,
|
||||
method: "PATCH",
|
||||
body: data,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags SpendingPlan
|
||||
* @name SpendingPlanDelete
|
||||
* @request DELETE:/api/SpendingPlan
|
||||
* @response `200` `void` Success
|
||||
*/
|
||||
spendingPlanDelete = (
|
||||
query?: {
|
||||
/** @format uuid */
|
||||
Id?: string;
|
||||
},
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/SpendingPlan`,
|
||||
method: "DELETE",
|
||||
query: query,
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags SpendingPlan
|
||||
* @name SpendingPlanFilterList
|
||||
* @request GET:/api/SpendingPlan/filter
|
||||
* @response `200` `(SpendingPlanViewModel)[]` Success
|
||||
*/
|
||||
spendingPlanFilterList = (
|
||||
query?: {
|
||||
/** @format uuid */
|
||||
Id?: string;
|
||||
},
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
this.request<SpendingPlanViewModel[], any>({
|
||||
path: `/api/SpendingPlan/filter`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags User
|
||||
* @name UserList
|
||||
* @request GET:/api/User
|
||||
* @response `200` `UserViewModel` Success
|
||||
*/
|
||||
userGet = (
|
||||
query?: {
|
||||
/** @format uuid */
|
||||
Id?: string;
|
||||
Name?: string;
|
||||
},
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
this.request<UserViewModel, any>({
|
||||
path: `/api/User`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags User
|
||||
* @name UserPartialUpdate
|
||||
* @request PATCH:/api/User
|
||||
* @response `200` `UserViewModel` Success
|
||||
*/
|
||||
userPartialUpdate = (data: UserDto, params: RequestParams = {}) =>
|
||||
this.request<UserViewModel, any>({
|
||||
path: `/api/User`,
|
||||
method: "PATCH",
|
||||
body: data,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags User
|
||||
* @name UserDelete
|
||||
* @request DELETE:/api/User
|
||||
* @response `200` `UserViewModel` Success
|
||||
*/
|
||||
userDelete = (
|
||||
query?: {
|
||||
/** @format uuid */
|
||||
Id?: string;
|
||||
Name?: string;
|
||||
},
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
this.request<UserViewModel, any>({
|
||||
path: `/api/User`,
|
||||
method: "DELETE",
|
||||
query: query,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
}
|
98
front/src/core/api/data-contracts.ts
Normal file
98
front/src/core/api/data-contracts.ts
Normal file
@ -0,0 +1,98 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
/*
|
||||
* ---------------------------------------------------------------
|
||||
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||
* ## ##
|
||||
* ## AUTHOR: acacode ##
|
||||
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export interface ChangeRecordDto {
|
||||
/** @format uuid */
|
||||
id?: string;
|
||||
/** @format uuid */
|
||||
userId?: string;
|
||||
/** @format uuid */
|
||||
spendingGroupId?: string | null;
|
||||
spendingGroupName?: string | null;
|
||||
/** @format double */
|
||||
sum?: number;
|
||||
/** @format date-time */
|
||||
changedAt?: string;
|
||||
}
|
||||
|
||||
export interface ChangeRecordViewModel {
|
||||
/** @format uuid */
|
||||
id?: string;
|
||||
/** @format double */
|
||||
sum?: number;
|
||||
/** @format date-time */
|
||||
changedAt?: string;
|
||||
spendingGroupName?: string | null;
|
||||
}
|
||||
|
||||
export interface SpendingGroupDto {
|
||||
/** @format uuid */
|
||||
id?: string;
|
||||
name?: string | null;
|
||||
/** @format uuid */
|
||||
userId?: string;
|
||||
changeRecords?: ChangeRecordDto[] | null;
|
||||
spendingPlans?: SpendingPlanDto[] | null;
|
||||
}
|
||||
|
||||
export interface SpendingGroupViewModel {
|
||||
/** @format uuid */
|
||||
id?: string;
|
||||
name?: string | null;
|
||||
changeRecords?: ChangeRecordViewModel[] | null;
|
||||
spendingPlans?: SpendingPlanViewModel[] | null;
|
||||
}
|
||||
|
||||
export interface SpendingPlanDto {
|
||||
/** @format uuid */
|
||||
id?: string;
|
||||
/** @format uuid */
|
||||
spendingGroupId?: string;
|
||||
/** @format double */
|
||||
sum?: number;
|
||||
/** @format date-time */
|
||||
startAt?: string;
|
||||
/** @format date-time */
|
||||
endAt?: string;
|
||||
}
|
||||
|
||||
export interface SpendingPlanViewModel {
|
||||
/** @format uuid */
|
||||
id?: string;
|
||||
/** @format date-time */
|
||||
startAt?: string;
|
||||
/** @format date-time */
|
||||
endAt?: string;
|
||||
/** @format double */
|
||||
sum?: number;
|
||||
}
|
||||
|
||||
export interface UserDto {
|
||||
/** @format uuid */
|
||||
id?: string;
|
||||
name?: string | null;
|
||||
password?: string | null;
|
||||
/** @format double */
|
||||
balance?: number;
|
||||
}
|
||||
|
||||
export interface UserLoginDto {
|
||||
name?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
|
||||
export interface UserViewModel {
|
||||
/** @format uuid */
|
||||
id?: string;
|
||||
name?: string | null;
|
||||
/** @format double */
|
||||
balance?: number;
|
||||
}
|
220
front/src/core/api/http-client.ts
Normal file
220
front/src/core/api/http-client.ts
Normal file
@ -0,0 +1,220 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
/*
|
||||
* ---------------------------------------------------------------
|
||||
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||
* ## ##
|
||||
* ## AUTHOR: acacode ##
|
||||
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export type QueryParamsType = Record<string | number, any>;
|
||||
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
|
||||
|
||||
export interface FullRequestParams extends Omit<RequestInit, "body"> {
|
||||
/** set parameter to `true` for call `securityWorker` for this request */
|
||||
secure?: boolean;
|
||||
/** request path */
|
||||
path: string;
|
||||
/** content type of request body */
|
||||
type?: ContentType;
|
||||
/** query params */
|
||||
query?: QueryParamsType;
|
||||
/** format of response (i.e. response.json() -> format: "json") */
|
||||
format?: ResponseFormat;
|
||||
/** request body */
|
||||
body?: unknown;
|
||||
/** base url */
|
||||
baseUrl?: string;
|
||||
/** request cancellation token */
|
||||
cancelToken?: CancelToken;
|
||||
}
|
||||
|
||||
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">;
|
||||
|
||||
export interface ApiConfig<SecurityDataType = unknown> {
|
||||
baseUrl?: string;
|
||||
baseApiParams?: Omit<RequestParams, "baseUrl" | "cancelToken" | "signal">;
|
||||
securityWorker?: (securityData: SecurityDataType | null) => Promise<RequestParams | void> | RequestParams | void;
|
||||
customFetch?: typeof fetch;
|
||||
}
|
||||
|
||||
export interface HttpResponse<D extends unknown, E extends unknown = unknown> extends Response {
|
||||
data: D;
|
||||
error: E;
|
||||
}
|
||||
|
||||
type CancelToken = Symbol | string | number;
|
||||
|
||||
export enum ContentType {
|
||||
Json = "application/json",
|
||||
FormData = "multipart/form-data",
|
||||
UrlEncoded = "application/x-www-form-urlencoded",
|
||||
Text = "text/plain",
|
||||
}
|
||||
|
||||
export class HttpClient<SecurityDataType = unknown> {
|
||||
public baseUrl: string = import.meta.env.VITE_API_URL;
|
||||
private securityData: SecurityDataType | null = null;
|
||||
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
|
||||
private abortControllers = new Map<CancelToken, AbortController>();
|
||||
private customFetch = (...fetchParams: Parameters<typeof fetch>) => fetch(...fetchParams);
|
||||
|
||||
private baseApiParams: RequestParams = {
|
||||
credentials: "same-origin",
|
||||
headers: {},
|
||||
redirect: "follow",
|
||||
referrerPolicy: "no-referrer",
|
||||
};
|
||||
|
||||
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
|
||||
Object.assign(this, apiConfig);
|
||||
}
|
||||
|
||||
public setSecurityData = (data: SecurityDataType | null) => {
|
||||
this.securityData = data;
|
||||
};
|
||||
|
||||
protected encodeQueryParam(key: string, value: any) {
|
||||
const encodedKey = encodeURIComponent(key);
|
||||
return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
|
||||
}
|
||||
|
||||
protected addQueryParam(query: QueryParamsType, key: string) {
|
||||
return this.encodeQueryParam(key, query[key]);
|
||||
}
|
||||
|
||||
protected addArrayQueryParam(query: QueryParamsType, key: string) {
|
||||
const value = query[key];
|
||||
return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
|
||||
}
|
||||
|
||||
protected toQueryString(rawQuery?: QueryParamsType): string {
|
||||
const query = rawQuery || {};
|
||||
const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]);
|
||||
return keys
|
||||
.map((key) => (Array.isArray(query[key]) ? this.addArrayQueryParam(query, key) : this.addQueryParam(query, key)))
|
||||
.join("&");
|
||||
}
|
||||
|
||||
protected addQueryParams(rawQuery?: QueryParamsType): string {
|
||||
const queryString = this.toQueryString(rawQuery);
|
||||
return queryString ? `?${queryString}` : "";
|
||||
}
|
||||
|
||||
private contentFormatters: Record<ContentType, (input: any) => any> = {
|
||||
[ContentType.Json]: (input: any) =>
|
||||
input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input,
|
||||
[ContentType.Text]: (input: any) => (input !== null && typeof input !== "string" ? JSON.stringify(input) : input),
|
||||
[ContentType.FormData]: (input: any) =>
|
||||
Object.keys(input || {}).reduce((formData, key) => {
|
||||
const property = input[key];
|
||||
formData.append(
|
||||
key,
|
||||
property instanceof Blob
|
||||
? property
|
||||
: typeof property === "object" && property !== null
|
||||
? JSON.stringify(property)
|
||||
: `${property}`,
|
||||
);
|
||||
return formData;
|
||||
}, new FormData()),
|
||||
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
|
||||
};
|
||||
|
||||
protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams {
|
||||
return {
|
||||
...this.baseApiParams,
|
||||
...params1,
|
||||
...(params2 || {}),
|
||||
headers: {
|
||||
...(this.baseApiParams.headers || {}),
|
||||
...(params1.headers || {}),
|
||||
...((params2 && params2.headers) || {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => {
|
||||
if (this.abortControllers.has(cancelToken)) {
|
||||
const abortController = this.abortControllers.get(cancelToken);
|
||||
if (abortController) {
|
||||
return abortController.signal;
|
||||
}
|
||||
return void 0;
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
this.abortControllers.set(cancelToken, abortController);
|
||||
return abortController.signal;
|
||||
};
|
||||
|
||||
public abortRequest = (cancelToken: CancelToken) => {
|
||||
const abortController = this.abortControllers.get(cancelToken);
|
||||
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
this.abortControllers.delete(cancelToken);
|
||||
}
|
||||
};
|
||||
|
||||
public request = async <T = any, E = any>({
|
||||
body,
|
||||
secure,
|
||||
path,
|
||||
type,
|
||||
query,
|
||||
format,
|
||||
baseUrl,
|
||||
cancelToken,
|
||||
...params
|
||||
}: FullRequestParams): Promise<HttpResponse<T, E>> => {
|
||||
const secureParams =
|
||||
((typeof secure === "boolean" ? secure : this.baseApiParams.secure) &&
|
||||
this.securityWorker &&
|
||||
(await this.securityWorker(this.securityData))) ||
|
||||
{};
|
||||
const requestParams = this.mergeRequestParams(params, secureParams);
|
||||
const queryString = query && this.toQueryString(query);
|
||||
const payloadFormatter = this.contentFormatters[type || ContentType.Json];
|
||||
const responseFormat = format || requestParams.format;
|
||||
|
||||
return this.customFetch(`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`, {
|
||||
...requestParams,
|
||||
headers: {
|
||||
...(requestParams.headers || {}),
|
||||
...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}),
|
||||
},
|
||||
signal: (cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal) || null,
|
||||
body: typeof body === "undefined" || body === null ? null : payloadFormatter(body),
|
||||
}).then(async (response) => {
|
||||
const r = response.clone() as HttpResponse<T, E>;
|
||||
r.data = null as unknown as T;
|
||||
r.error = null as unknown as E;
|
||||
|
||||
const data = !responseFormat
|
||||
? r
|
||||
: await response[responseFormat]()
|
||||
.then((data) => {
|
||||
if (r.ok) {
|
||||
r.data = data;
|
||||
} else {
|
||||
r.error = data;
|
||||
}
|
||||
return r;
|
||||
})
|
||||
.catch((e) => {
|
||||
r.error = e;
|
||||
return r;
|
||||
});
|
||||
|
||||
if (cancelToken) {
|
||||
this.abortControllers.delete(cancelToken);
|
||||
}
|
||||
|
||||
if (!response.ok) throw data;
|
||||
return data;
|
||||
});
|
||||
};
|
||||
}
|
30
front/src/core/services/auth-service.ts
Normal file
30
front/src/core/services/auth-service.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { useUserStore } from "../../store";
|
||||
import { Api } from "../api/Api";
|
||||
import { UserDto, UserLoginDto, UserViewModel } from "../api/data-contracts";
|
||||
|
||||
export class AuthService {
|
||||
private readonly _api: Api;
|
||||
|
||||
constructor(api: Api) {
|
||||
this._api = api;
|
||||
|
||||
}
|
||||
async login(data: UserLoginDto) {
|
||||
let result = await this._api.auth(data);
|
||||
console.log(result);
|
||||
const store = useUserStore();
|
||||
store.updateUser(result.data);
|
||||
}
|
||||
|
||||
logout() {
|
||||
const store = useUserStore();
|
||||
store.updateUser({} as UserViewModel);
|
||||
}
|
||||
|
||||
async register(data: UserDto) {
|
||||
let result = await this._api.authRegister(data);
|
||||
const store = useUserStore();
|
||||
store.updateUser(result.data);
|
||||
}
|
||||
|
||||
}
|
42
front/src/core/services/change-record-service.ts
Normal file
42
front/src/core/services/change-record-service.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { useUserStore } from "../../store";
|
||||
import { Api } from "../api/Api";
|
||||
import { ChangeRecordDto, ChangeRecordViewModel } from "../api/data-contracts";
|
||||
|
||||
export class ChangeRecordService {
|
||||
private readonly _api: Api
|
||||
constructor(api: Api) {
|
||||
this._api = api;
|
||||
}
|
||||
|
||||
private async _updateUser() {
|
||||
const store = useUserStore();
|
||||
let updatedUser = await this._api.userGet({ Id: store.user.id });
|
||||
|
||||
store.updateUser(updatedUser.data);
|
||||
}
|
||||
|
||||
async createRecord(data: ChangeRecordDto) {
|
||||
let result = await this._api.changeRecordCreate(data);
|
||||
this._updateUser();
|
||||
console.log(result);
|
||||
}
|
||||
|
||||
async deleteRecord(Id: string) {
|
||||
console.log(Id);
|
||||
let result = await this._api.changeRecordDelete({ Id });
|
||||
this._updateUser();
|
||||
console.log("delete");
|
||||
console.log(result);
|
||||
}
|
||||
|
||||
async getList(): Promise<ChangeRecordViewModel[] | null> {
|
||||
const store = useUserStore();
|
||||
if (!store.user.id) return null;
|
||||
|
||||
let UserId = store.user.id;
|
||||
let result = await this._api.changeRecordFilterList({ UserId });
|
||||
|
||||
console.log(result);
|
||||
return result.data;
|
||||
}
|
||||
}
|
33
front/src/core/services/group-service.ts
Normal file
33
front/src/core/services/group-service.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { useUserStore } from "../../store";
|
||||
import { Api } from "../api/Api";
|
||||
import { SpendingGroupDto, SpendingGroupViewModel } from "../api/data-contracts";
|
||||
|
||||
export class GroupService {
|
||||
private readonly _api: Api;
|
||||
constructor(api: Api) {
|
||||
this._api = api;
|
||||
}
|
||||
|
||||
async getList(): Promise<SpendingGroupViewModel[] | null> {
|
||||
const store = useUserStore();
|
||||
console.log("get list " + store.user.id);
|
||||
if (!store.user.id) return null;
|
||||
|
||||
let UserId = store.user.id;
|
||||
let result = await this._api.spendingGroupFilterList({ UserId });
|
||||
|
||||
console.log(result);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
async deleteGroup(Id: string) {
|
||||
let result = await this._api.spendingGroupDelete({ Id });
|
||||
console.log("delete");
|
||||
console.log(result);
|
||||
}
|
||||
|
||||
async createGroup(data: SpendingGroupDto) {
|
||||
let result = await this._api.spendingGroupCreate(data);
|
||||
console.log(result);
|
||||
}
|
||||
}
|
26
front/src/core/services/plans-service.ts
Normal file
26
front/src/core/services/plans-service.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Api } from "../api/Api";
|
||||
import { SpendingPlanDto, SpendingPlanViewModel } from "../api/data-contracts";
|
||||
|
||||
export class PlanService {
|
||||
|
||||
private readonly _api: Api
|
||||
constructor(api: Api) {
|
||||
this._api = api;
|
||||
}
|
||||
|
||||
async deletePlan(Id: string) {
|
||||
let result = await this._api.spendingPlanDelete({ Id });
|
||||
console.log("delete");
|
||||
console.log(result);
|
||||
}
|
||||
|
||||
async getList(groupId: string): Promise<SpendingPlanViewModel[] | null> {
|
||||
const result = await this._api.spendingGroupDetail(groupId);
|
||||
return result.data.spendingPlans || null;
|
||||
}
|
||||
|
||||
async createPlan(data: SpendingPlanDto) {
|
||||
const result = await this._api.spendingPlanCreate(data);
|
||||
console.log(result);
|
||||
}
|
||||
}
|
@ -1,5 +1,24 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { Api } from './core/api/Api'
|
||||
import { createPinia } from 'pinia'
|
||||
import { AuthService } from './core/services/auth-service'
|
||||
import { ChangeRecordService } from './core/services/change-record-service'
|
||||
import { GroupService } from './core/services/group-service'
|
||||
import { PlanService } from './core/services/plans-service'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router)
|
||||
app.use(createPinia())
|
||||
|
||||
// Di
|
||||
const api = new Api();
|
||||
app.provide(AuthService.name, new AuthService(api));
|
||||
app.provide(ChangeRecordService.name, new ChangeRecordService(api));
|
||||
app.provide(GroupService.name, new GroupService(api));
|
||||
app.provide(PlanService.name, new PlanService(api));
|
||||
|
||||
app.mount('#app')
|
||||
|
46
front/src/router.ts
Normal file
46
front/src/router.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import { useUserStore } from './store';
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: () => import('./components/pages/Home.vue'),
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('./components/pages/Login.vue'),
|
||||
meta: { notRequiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/signup',
|
||||
name: 'signup',
|
||||
component: () => import('./components/pages/SignUp.vue'),
|
||||
meta: { notRequiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/groups',
|
||||
name: 'groups',
|
||||
component: () => import('./components/pages/Groups.vue'),
|
||||
},
|
||||
{
|
||||
path: '/plans/:groupId',
|
||||
name: 'plans',
|
||||
component: () => import('./components/pages/Plans.vue'),
|
||||
}
|
||||
],
|
||||
});
|
||||
|
||||
router.beforeEach((to, _, next) => {
|
||||
const store = useUserStore();
|
||||
if (!to.meta.notRequiresAuth && !store.user.id) {
|
||||
next({ name: 'login' });
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
19
front/src/store.ts
Normal file
19
front/src/store.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { UserViewModel } from './core/api/data-contracts'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export interface State {
|
||||
user: UserViewModel
|
||||
}
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: (): State => ({
|
||||
user: {} as UserViewModel
|
||||
}),
|
||||
actions: {
|
||||
updateUser(payload: UserViewModel) {
|
||||
if (!payload) return;
|
||||
|
||||
this.user = payload;
|
||||
},
|
||||
}
|
||||
})
|
@ -1,7 +1,18 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
plugins: [
|
||||
vue(),
|
||||
Components({
|
||||
resolvers: [
|
||||
AntDesignVueResolver({
|
||||
importStyle: false, // css in js
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user