Task_7.5_UI_Reports #11

Merged
chillya merged 19 commits from Task_7.5_UI_Reports into main 2025-05-22 12:31:02 +04:00
218 changed files with 16697 additions and 789 deletions

View File

@@ -72,7 +72,7 @@ public class ReportContract(IClientStorageContract clientStorage, ICurrencyStora
return _baseWordBuilder
.AddHeader("Клиенты по кредитным программам")
.AddParagraph($"Сформировано на дату {DateTime.Now}")
.AddTable([3000, 3000, 3000, 3000], tableRows)
.AddTable([100, 100, 100, 100], tableRows)
.Build();
}
@@ -90,20 +90,20 @@ public class ReportContract(IClientStorageContract clientStorage, ICurrencyStora
{
for (int i = 0; i < program.ClientSurname.Count; i++)
{
tableRows.Add(new string[]
{
tableRows.Add(
[
program.CreditProgramName,
program.ClientSurname[i],
program.ClientName[i],
program.ClientBalance[i].ToString("N2")
});
]);
}
}
return _baseExcelBuilder
.AddHeader("Клиенты по кредитным программам", 0, 4)
.AddParagraph($"Сформировано на дату {DateTime.Now}", 0)
.AddTable([3000, 3000, 3000, 3000], tableRows)
.AddTable([25, 25, 25, 25], tableRows)
.Build();
}
@@ -169,21 +169,21 @@ public class ReportContract(IClientStorageContract clientStorage, ICurrencyStora
foreach (var client in data)
{
tableRows.Add(new string[]
{
tableRows.Add(
[
client.ClientSurname,
client.ClientName,
client.ClientBalance.ToString("N2"),
client.DepositRate.ToString("N2"),
$"{client.DepositPeriod} мес.",
$"{client.FromPeriod.ToShortDateString()} - {client.ToPeriod.ToShortDateString()}"
});
]);
}
return _basePdfBuilder
.AddHeader("Клиенты по вкладам")
.AddParagraph($"за период с {dateStart.ToShortDateString()} по {dateFinish.ToShortDateString()}")
.AddTable([80, 80, 80, 80, 80, 80], tableRows)
.AddTable([25, 25, 25, 25, 25, 25], tableRows)
.Build();
}
@@ -229,23 +229,54 @@ public class ReportContract(IClientStorageContract clientStorage, ICurrencyStora
foreach (var currency in data)
{
// Вывод информации по кредитным программам
for (int i = 0; i < currency.CreditProgramName.Count; i++)
{
tableRows.Add(new string[]
// Вычисляем индекс депозита, если есть соответствующие
string depositRate = "—";
string depositPeriod = "—";
// Проверяем, есть ли депозиты для этой валюты и не вышли ли мы за границы массива
if (currency.DepositRate.Count > 0)
{
// Берем индекс по модулю, чтобы не выйти за границы массива
int depositIndex = i % currency.DepositRate.Count;
depositRate = currency.DepositRate[depositIndex].ToString("N2");
depositPeriod = $"{currency.DepositPeriod[depositIndex]} мес.";
}
// Добавляем строку в таблицу
tableRows.Add(
[
currency.CurrencyName,
currency.CreditProgramName[i],
currency.CreditProgramMaxCost[i].ToString("N2"),
currency.DepositRate[i].ToString("N2"),
$"{currency.DepositPeriod[i]} мес."
});
depositRate,
depositPeriod
]);
}
// Если есть депозиты, но нет кредитных программ, добавляем строки только с депозитами
if (currency.CreditProgramName.Count == 0 && currency.DepositRate.Count > 0)
{
for (int j = 0; j < currency.DepositRate.Count; j++)
{
tableRows.Add(
[
currency.CurrencyName,
"—",
"—",
currency.DepositRate[j].ToString("N2"),
$"{currency.DepositPeriod[j]} мес."
]);
}
}
}
return _basePdfBuilder
.AddHeader("Вклады и кредитные программы по валютам")
.AddParagraph($"за период с {dateStart.ToShortDateString()} по {dateFinish.ToShortDateString()}")
.AddTable([80, 100, 80, 80, 80], tableRows)
.AddTable([25, 30, 25, 25, 25], tableRows)
.Build();
}
@@ -284,20 +315,20 @@ public class ReportContract(IClientStorageContract clientStorage, ICurrencyStora
{
for (int i = 0; i < program.DepositRate.Count; i++)
{
tableRows.Add(new string[]
{
tableRows.Add(
[
program.CreditProgramName,
program.DepositRate[i].ToString("N2"),
program.DepositCost[i].ToString("N2"),
program.DepositPeriod[i].ToString()
});
]);
}
}
return _baseWordBuilder
.AddHeader("Вклады по кредитным программам")
.AddParagraph($"Сформировано на дату {DateTime.Now}")
.AddTable([3000, 3000, 3000, 3000], tableRows)
.AddTable([2000, 2000, 2000, 2000], tableRows)
.Build();
}
@@ -315,20 +346,20 @@ public class ReportContract(IClientStorageContract clientStorage, ICurrencyStora
{
for (int i = 0; i < program.DepositRate.Count; i++)
{
tableRows.Add(new string[]
{
tableRows.Add(
[
program.CreditProgramName,
program.DepositRate[i].ToString("N2"),
program.DepositCost[i].ToString("N2"),
program.DepositPeriod[i].ToString()
});
]);
}
}
return _baseExcelBuilder
.AddHeader("Вклады по кредитным программам", 0, 4)
.AddParagraph($"Сформировано на дату {DateTime.Now}", 0)
.AddTable([3000, 3000, 3000, 3000], tableRows)
.AddTable([25, 25, 25, 25], tableRows)
.Build();
}
}

View File

@@ -56,7 +56,7 @@ public class MigraDocPdfBuilder : BasePdfBuilder
// Добавляем столбцы с заданной шириной
foreach (var width in columnsWidths)
{
var widthInCm = width / 28.35;
var widthInCm = width / 10.35;
var column = table.AddColumn(Unit.FromCentimeter(widthInCm));
column.Format.Alignment = ParagraphAlignment.Left;
}

View File

@@ -15,4 +15,6 @@ public interface IClerkAdapter
ClerkOperationResponse RegisterClerk(ClerkBindingModel clerkModel);
ClerkOperationResponse ChangeClerkInfo(ClerkBindingModel clerkModel);
ClerkOperationResponse Login(LoginBindingModel clerkModel, out string token);
}

View File

@@ -15,4 +15,6 @@ public interface IStorekeeperAdapter
StorekeeperOperationResponse RegisterStorekeeper(StorekeeperBindingModel storekeeperModel);
StorekeeperOperationResponse ChangeStorekeeperInfo(StorekeeperBindingModel storekeeperModel);
StorekeeperOperationResponse Login(LoginBindingModel storekeeperModel, out string token);
}

View File

@@ -21,4 +21,7 @@ public class ClerkOperationResponse : OperationResponse
public static ClerkOperationResponse InternalServerError(string message) =>
InternalServerError<ClerkOperationResponse>(message);
public static ClerkOperationResponse Unauthorized(string message) =>
Unauthorized<ClerkOperationResponse>(message);
}

View File

@@ -18,4 +18,7 @@ public class ReportOperationResponse : OperationResponse
public static ReportOperationResponse BadRequest(string message) => BadRequest<ReportOperationResponse>(message);
public static ReportOperationResponse InternalServerError(string message) => InternalServerError<ReportOperationResponse>(message);
public Stream? GetStream() => Result as Stream;
public string? GetFileName() => FileName;
}

View File

@@ -8,6 +8,9 @@ public class StorekeeperOperationResponse : OperationResponse
public static StorekeeperOperationResponse OK(List<StorekeeperViewModel> data) =>
OK<StorekeeperOperationResponse, List<StorekeeperViewModel>>(data);
public static StorekeeperOperationResponse OK(string token) =>
OK<StorekeeperOperationResponse, string>(token);
public static StorekeeperOperationResponse OK(StorekeeperViewModel data) =>
OK<StorekeeperOperationResponse, StorekeeperViewModel>(data);
@@ -21,4 +24,7 @@ public class StorekeeperOperationResponse : OperationResponse
public static StorekeeperOperationResponse InternalServerError(string message) =>
InternalServerError<StorekeeperOperationResponse>(message);
public static StorekeeperOperationResponse Unauthorized(string message) =>
Unauthorized<StorekeeperOperationResponse>(message);
}

View File

@@ -0,0 +1,8 @@
namespace BankContracts.BindingModels;
public class LoginBindingModel
{
public required string Login { get; set; }
public required string Password { get; set; }
}

View File

@@ -2,9 +2,11 @@
public class MailSendInfoBindingModel
{
public string MailAddress { get; set; } = string.Empty;
public string ToEmail { get; set; } = string.Empty;
public string Subject { get; set; } = string.Empty;
public string Text { get; set; } = string.Empty;
public MemoryStream Attachment { get; set; } = new MemoryStream();
public string FileName { get; set; } = string.Empty;
public string Body { get; set; } = string.Empty;
public string? AttachmentPath { get; set; }
}

View File

@@ -58,4 +58,8 @@ public class OperationResponse
protected static TResult InternalServerError<TResult>(string? errorMessage = null)
where TResult : OperationResponse, new() =>
new() { StatusCode = HttpStatusCode.InternalServerError, Result = errorMessage };
protected static TResult Unauthorized<TResult>(string? errorMessage = null)
where TResult : OperationResponse, new() =>
new() { StatusCode = HttpStatusCode.Unauthorized, Result = errorMessage };
}

View File

@@ -9,6 +9,10 @@
<ItemGroup>
<PackageReference Include="AutoMapper" Version="14.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
</ItemGroup>

View File

@@ -33,7 +33,7 @@ internal class BankDbContext(IConfigurationDatabase configurationDatabase) : DbC
modelBuilder.Entity<CreditProgram>()
.HasIndex(x => x.Name)
.IsUnique();
modelBuilder.Entity<Currency>()
.HasIndex(x => x.Abbreviation)
.IsUnique();
@@ -80,17 +80,17 @@ internal class BankDbContext(IConfigurationDatabase configurationDatabase) : DbC
public DbSet<Clerk> Clerks { get; set; }
public DbSet<Client> Clients { get; set; }
public DbSet<CreditProgram> CreditPrograms { get; set; }
public DbSet<Currency> Currencies { get; set; }
public DbSet<Deposit> Deposits { get; set; }
public DbSet<Period> Periods { get; set; }
public DbSet<Replenishment> Replenishments { get; set; }
public DbSet<Storekeeper> Storekeepers { get; set; }
public DbSet<DepositCurrency> DepositCurrencies { get; set; }

View File

@@ -0,0 +1,17 @@
using BankContracts.Infrastructure;
using Microsoft.EntityFrameworkCore.Design;
namespace BankDatabase;
//public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<BankDbContext>
//{
// //public BankDbContext CreateDbContext(string[] args)
// //{
// // return new BankDbContext(new ConfigurationDatabase());
// //}
//}
internal class ConfigurationDatabase : IConfigurationDatabase
{
public string ConnectionString => "Host=127.0.0.1;Port=5432;Database=BankTest;Username=postgres;Password=admin123;";
}

View File

@@ -85,7 +85,7 @@ internal class ClerkStorageContract : IClerkStorageContract
_dbContext.Clerks.Add(_mapper.Map<Clerk>(clerkDataModel));
_dbContext.SaveChanges();
}
catch (InvalidOperationException ex) when (ex.TargetSite?.Name == "ThrowIdentityConflict")
catch (DbUpdateException ex) when (ex.InnerException is PostgresException { ConstraintName: "PK_Clerks" })
{
_dbContext.ChangeTracker.Clear();
throw new ElementExistsException($"Id {clerkDataModel.Id}");

View File

@@ -109,7 +109,7 @@ internal class DepositStorageContract : IDepositStorageContract
catch (InvalidOperationException ex) when (ex.TargetSite?.Name == "ThrowIdentityConflict")
{
_dbContext.ChangeTracker.Clear();
throw new ElementExistsException($"Id {depositDataModel.Id }");
throw new ElementExistsException($"Id {depositDataModel.Id}");
}
catch (DbUpdateException ex) when (ex.InnerException is PostgresException { ConstraintName: "IX_Deposits_InterestRate" })
{
@@ -127,40 +127,76 @@ internal class DepositStorageContract : IDepositStorageContract
{
try
{
var transaction = _dbContext.Database.BeginTransaction();
using var transaction = _dbContext.Database.BeginTransaction();
try
{
var element = GetDepositById(depositDataModel.Id) ?? throw new ElementNotFoundException(depositDataModel.Id);
// Загружаем существующий вклад со связями
var existingDeposit = _dbContext.Deposits
.Include(d => d.DepositCurrencies)
.FirstOrDefault(d => d.Id == depositDataModel.Id);
if (existingDeposit == null)
{
throw new ElementNotFoundException(depositDataModel.Id);
}
// Обновляем основные поля вклада
existingDeposit.InterestRate = depositDataModel.InterestRate;
existingDeposit.Cost = depositDataModel.Cost;
existingDeposit.Period = depositDataModel.Period;
existingDeposit.ClerkId = depositDataModel.ClerkId;
// Обновляем связи с валютами, если они переданы
if (depositDataModel.Currencies != null)
{
if (element.DepositCurrencies != null || element.DepositCurrencies?.Count >= 0)
// Удаляем все существующие связи
if (existingDeposit.DepositCurrencies != null)
{
_dbContext.DepositCurrencies.RemoveRange(element.DepositCurrencies);
_dbContext.DepositCurrencies.RemoveRange(existingDeposit.DepositCurrencies);
}
element.DepositCurrencies = _mapper.Map<List<DepositCurrency>>(depositDataModel.Currencies);
// Сохраняем изменения для применения удаления
_dbContext.SaveChanges();
// Создаем новые связи
existingDeposit.DepositCurrencies = depositDataModel.Currencies.Select(c =>
new DepositCurrency
{
DepositId = existingDeposit.Id,
CurrencyId = c.CurrencyId
}).ToList();
}
_mapper.Map(depositDataModel, element);
// Сохраняем все изменения
_dbContext.SaveChanges();
transaction.Commit();
// Выводим отладочную информацию
System.Console.WriteLine($"Updated deposit {existingDeposit.Id} with {existingDeposit.DepositCurrencies?.Count ?? 0} currency relations");
foreach (var relation in existingDeposit.DepositCurrencies ?? Enumerable.Empty<DepositCurrency>())
{
System.Console.WriteLine($"Currency relation: DepositId={relation.DepositId}, CurrencyId={relation.CurrencyId}");
}
}
catch
catch (Exception ex)
{
transaction.Rollback();
throw;
System.Console.WriteLine($"Error in transaction: {ex.Message}");
if (ex is ElementNotFoundException)
throw;
throw new StorageException(ex.Message);
}
}
catch (ElementNotFoundException)
{
_dbContext.ChangeTracker.Clear();
throw;
}
catch (DbUpdateException ex) when (ex.InnerException is PostgresException { ConstraintName: "IX_Deposits_InterestRate" })
{
_dbContext.ChangeTracker.Clear();
throw new ElementExistsException($"InterestRate {depositDataModel.InterestRate}");
}
catch (ElementNotFoundException)
{
_dbContext.ChangeTracker.Clear();
throw;
}
catch (Exception ex)
{
_dbContext.ChangeTracker.Clear();

View File

@@ -0,0 +1,562 @@
// <auto-generated />
using System;
using BankDatabase;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace BankDatabase.Migrations
{
[DbContext(typeof(BankDbContext))]
[Migration("20250518195627_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("BankDatabase.Models.Clerk", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Login")
.IsRequired()
.HasColumnType("text");
b.Property<string>("MiddleName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Surname")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Email")
.IsUnique();
b.HasIndex("Login")
.IsUnique();
b.HasIndex("PhoneNumber")
.IsUnique();
b.ToTable("Clerks");
});
modelBuilder.Entity("BankDatabase.Models.Client", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<decimal>("Balance")
.HasColumnType("numeric");
b.Property<string>("ClerkId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Surname")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("ClerkId");
b.ToTable("Clients");
});
modelBuilder.Entity("BankDatabase.Models.ClientCreditProgram", b =>
{
b.Property<string>("ClientId")
.HasColumnType("text");
b.Property<string>("CreditProgramId")
.HasColumnType("text");
b.HasKey("ClientId", "CreditProgramId");
b.HasIndex("CreditProgramId");
b.ToTable("CreditProgramClients");
});
modelBuilder.Entity("BankDatabase.Models.CreditProgram", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<decimal>("Cost")
.HasColumnType("numeric");
b.Property<decimal>("MaxCost")
.HasColumnType("numeric");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PeriodId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("StorekeeperId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.HasIndex("PeriodId");
b.HasIndex("StorekeeperId");
b.ToTable("CreditPrograms");
});
modelBuilder.Entity("BankDatabase.Models.CreditProgramCurrency", b =>
{
b.Property<string>("CreditProgramId")
.HasColumnType("text");
b.Property<string>("CurrencyId")
.HasColumnType("text");
b.HasKey("CreditProgramId", "CurrencyId");
b.HasIndex("CurrencyId");
b.ToTable("CurrencyCreditPrograms");
});
modelBuilder.Entity("BankDatabase.Models.Currency", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("Abbreviation")
.IsRequired()
.HasColumnType("text");
b.Property<decimal>("Cost")
.HasColumnType("numeric");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("StorekeeperId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Abbreviation")
.IsUnique();
b.HasIndex("StorekeeperId");
b.ToTable("Currencies");
});
modelBuilder.Entity("BankDatabase.Models.Deposit", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ClerkId")
.IsRequired()
.HasColumnType("text");
b.Property<decimal>("Cost")
.HasColumnType("numeric");
b.Property<float>("InterestRate")
.HasColumnType("real");
b.Property<int>("Period")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ClerkId");
b.ToTable("Deposits");
});
modelBuilder.Entity("BankDatabase.Models.DepositClient", b =>
{
b.Property<string>("DepositId")
.HasColumnType("text");
b.Property<string>("ClientId")
.HasColumnType("text");
b.HasKey("DepositId", "ClientId");
b.HasIndex("ClientId");
b.ToTable("DepositClients");
});
modelBuilder.Entity("BankDatabase.Models.DepositCurrency", b =>
{
b.Property<string>("DepositId")
.HasColumnType("text");
b.Property<string>("CurrencyId")
.HasColumnType("text");
b.HasKey("DepositId", "CurrencyId");
b.HasIndex("CurrencyId");
b.ToTable("DepositCurrencies");
});
modelBuilder.Entity("BankDatabase.Models.Period", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<DateTime>("EndTime")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("StartTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("StorekeeperId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("StorekeeperId");
b.ToTable("Periods");
});
modelBuilder.Entity("BankDatabase.Models.Replenishment", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<decimal>("Amount")
.HasColumnType("numeric");
b.Property<string>("ClerkId")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<string>("DepositId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("ClerkId");
b.HasIndex("DepositId");
b.ToTable("Replenishments");
});
modelBuilder.Entity("BankDatabase.Models.Storekeeper", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Login")
.IsRequired()
.HasColumnType("text");
b.Property<string>("MiddleName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Surname")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Email")
.IsUnique();
b.HasIndex("Login")
.IsUnique();
b.HasIndex("PhoneNumber")
.IsUnique();
b.ToTable("Storekeepers");
});
modelBuilder.Entity("BankDatabase.Models.Client", b =>
{
b.HasOne("BankDatabase.Models.Clerk", "Clerk")
.WithMany("Clients")
.HasForeignKey("ClerkId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Clerk");
});
modelBuilder.Entity("BankDatabase.Models.ClientCreditProgram", b =>
{
b.HasOne("BankDatabase.Models.Client", "Client")
.WithMany("CreditProgramClients")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("BankDatabase.Models.CreditProgram", "CreditProgram")
.WithMany("CreditProgramClients")
.HasForeignKey("CreditProgramId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
b.Navigation("CreditProgram");
});
modelBuilder.Entity("BankDatabase.Models.CreditProgram", b =>
{
b.HasOne("BankDatabase.Models.Period", "Period")
.WithMany("CreditPrograms")
.HasForeignKey("PeriodId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("BankDatabase.Models.Storekeeper", "Storekeeper")
.WithMany("CreditPrograms")
.HasForeignKey("StorekeeperId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Period");
b.Navigation("Storekeeper");
});
modelBuilder.Entity("BankDatabase.Models.CreditProgramCurrency", b =>
{
b.HasOne("BankDatabase.Models.CreditProgram", "CreditProgram")
.WithMany("CurrencyCreditPrograms")
.HasForeignKey("CreditProgramId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("BankDatabase.Models.Currency", "Currency")
.WithMany("CurrencyCreditPrograms")
.HasForeignKey("CurrencyId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("CreditProgram");
b.Navigation("Currency");
});
modelBuilder.Entity("BankDatabase.Models.Currency", b =>
{
b.HasOne("BankDatabase.Models.Storekeeper", "Storekeeper")
.WithMany("Currencies")
.HasForeignKey("StorekeeperId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Storekeeper");
});
modelBuilder.Entity("BankDatabase.Models.Deposit", b =>
{
b.HasOne("BankDatabase.Models.Clerk", "Clerk")
.WithMany("Deposits")
.HasForeignKey("ClerkId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Clerk");
});
modelBuilder.Entity("BankDatabase.Models.DepositClient", b =>
{
b.HasOne("BankDatabase.Models.Client", "Client")
.WithMany("DepositClients")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("BankDatabase.Models.Deposit", "Deposit")
.WithMany("DepositClients")
.HasForeignKey("DepositId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
b.Navigation("Deposit");
});
modelBuilder.Entity("BankDatabase.Models.DepositCurrency", b =>
{
b.HasOne("BankDatabase.Models.Currency", "Currency")
.WithMany("DepositCurrencies")
.HasForeignKey("CurrencyId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("BankDatabase.Models.Deposit", "Deposit")
.WithMany("DepositCurrencies")
.HasForeignKey("DepositId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Currency");
b.Navigation("Deposit");
});
modelBuilder.Entity("BankDatabase.Models.Period", b =>
{
b.HasOne("BankDatabase.Models.Storekeeper", "Storekeeper")
.WithMany("Periods")
.HasForeignKey("StorekeeperId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Storekeeper");
});
modelBuilder.Entity("BankDatabase.Models.Replenishment", b =>
{
b.HasOne("BankDatabase.Models.Clerk", "Clerk")
.WithMany("Replenishments")
.HasForeignKey("ClerkId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("BankDatabase.Models.Deposit", "Deposit")
.WithMany("Replenishments")
.HasForeignKey("DepositId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Clerk");
b.Navigation("Deposit");
});
modelBuilder.Entity("BankDatabase.Models.Clerk", b =>
{
b.Navigation("Clients");
b.Navigation("Deposits");
b.Navigation("Replenishments");
});
modelBuilder.Entity("BankDatabase.Models.Client", b =>
{
b.Navigation("CreditProgramClients");
b.Navigation("DepositClients");
});
modelBuilder.Entity("BankDatabase.Models.CreditProgram", b =>
{
b.Navigation("CreditProgramClients");
b.Navigation("CurrencyCreditPrograms");
});
modelBuilder.Entity("BankDatabase.Models.Currency", b =>
{
b.Navigation("CurrencyCreditPrograms");
b.Navigation("DepositCurrencies");
});
modelBuilder.Entity("BankDatabase.Models.Deposit", b =>
{
b.Navigation("DepositClients");
b.Navigation("DepositCurrencies");
b.Navigation("Replenishments");
});
modelBuilder.Entity("BankDatabase.Models.Period", b =>
{
b.Navigation("CreditPrograms");
});
modelBuilder.Entity("BankDatabase.Models.Storekeeper", b =>
{
b.Navigation("CreditPrograms");
b.Navigation("Currencies");
b.Navigation("Periods");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,433 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BankDatabase.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Clerks",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Surname = table.Column<string>(type: "text", nullable: false),
MiddleName = table.Column<string>(type: "text", nullable: false),
Login = table.Column<string>(type: "text", nullable: false),
Password = table.Column<string>(type: "text", nullable: false),
Email = table.Column<string>(type: "text", nullable: false),
PhoneNumber = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Clerks", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Storekeepers",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Surname = table.Column<string>(type: "text", nullable: false),
MiddleName = table.Column<string>(type: "text", nullable: false),
Login = table.Column<string>(type: "text", nullable: false),
Password = table.Column<string>(type: "text", nullable: false),
Email = table.Column<string>(type: "text", nullable: false),
PhoneNumber = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Storekeepers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Clients",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Surname = table.Column<string>(type: "text", nullable: false),
Balance = table.Column<decimal>(type: "numeric", nullable: false),
ClerkId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Clients", x => x.Id);
table.ForeignKey(
name: "FK_Clients_Clerks_ClerkId",
column: x => x.ClerkId,
principalTable: "Clerks",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "Deposits",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
InterestRate = table.Column<float>(type: "real", nullable: false),
Cost = table.Column<decimal>(type: "numeric", nullable: false),
Period = table.Column<int>(type: "integer", nullable: false),
ClerkId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Deposits", x => x.Id);
table.ForeignKey(
name: "FK_Deposits_Clerks_ClerkId",
column: x => x.ClerkId,
principalTable: "Clerks",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "Currencies",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Abbreviation = table.Column<string>(type: "text", nullable: false),
Cost = table.Column<decimal>(type: "numeric", nullable: false),
StorekeeperId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Currencies", x => x.Id);
table.ForeignKey(
name: "FK_Currencies_Storekeepers_StorekeeperId",
column: x => x.StorekeeperId,
principalTable: "Storekeepers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Periods",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
StartTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
EndTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
StorekeeperId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Periods", x => x.Id);
table.ForeignKey(
name: "FK_Periods_Storekeepers_StorekeeperId",
column: x => x.StorekeeperId,
principalTable: "Storekeepers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "DepositClients",
columns: table => new
{
DepositId = table.Column<string>(type: "text", nullable: false),
ClientId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DepositClients", x => new { x.DepositId, x.ClientId });
table.ForeignKey(
name: "FK_DepositClients_Clients_ClientId",
column: x => x.ClientId,
principalTable: "Clients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_DepositClients_Deposits_DepositId",
column: x => x.DepositId,
principalTable: "Deposits",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Replenishments",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Amount = table.Column<decimal>(type: "numeric", nullable: false),
Date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DepositId = table.Column<string>(type: "text", nullable: false),
ClerkId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Replenishments", x => x.Id);
table.ForeignKey(
name: "FK_Replenishments_Clerks_ClerkId",
column: x => x.ClerkId,
principalTable: "Clerks",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Replenishments_Deposits_DepositId",
column: x => x.DepositId,
principalTable: "Deposits",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "DepositCurrencies",
columns: table => new
{
DepositId = table.Column<string>(type: "text", nullable: false),
CurrencyId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DepositCurrencies", x => new { x.DepositId, x.CurrencyId });
table.ForeignKey(
name: "FK_DepositCurrencies_Currencies_CurrencyId",
column: x => x.CurrencyId,
principalTable: "Currencies",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_DepositCurrencies_Deposits_DepositId",
column: x => x.DepositId,
principalTable: "Deposits",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "CreditPrograms",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Cost = table.Column<decimal>(type: "numeric", nullable: false),
MaxCost = table.Column<decimal>(type: "numeric", nullable: false),
StorekeeperId = table.Column<string>(type: "text", nullable: false),
PeriodId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CreditPrograms", x => x.Id);
table.ForeignKey(
name: "FK_CreditPrograms_Periods_PeriodId",
column: x => x.PeriodId,
principalTable: "Periods",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_CreditPrograms_Storekeepers_StorekeeperId",
column: x => x.StorekeeperId,
principalTable: "Storekeepers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "CreditProgramClients",
columns: table => new
{
ClientId = table.Column<string>(type: "text", nullable: false),
CreditProgramId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CreditProgramClients", x => new { x.ClientId, x.CreditProgramId });
table.ForeignKey(
name: "FK_CreditProgramClients_Clients_ClientId",
column: x => x.ClientId,
principalTable: "Clients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_CreditProgramClients_CreditPrograms_CreditProgramId",
column: x => x.CreditProgramId,
principalTable: "CreditPrograms",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "CurrencyCreditPrograms",
columns: table => new
{
CreditProgramId = table.Column<string>(type: "text", nullable: false),
CurrencyId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CurrencyCreditPrograms", x => new { x.CreditProgramId, x.CurrencyId });
table.ForeignKey(
name: "FK_CurrencyCreditPrograms_CreditPrograms_CreditProgramId",
column: x => x.CreditProgramId,
principalTable: "CreditPrograms",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_CurrencyCreditPrograms_Currencies_CurrencyId",
column: x => x.CurrencyId,
principalTable: "Currencies",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Clerks_Email",
table: "Clerks",
column: "Email",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Clerks_Login",
table: "Clerks",
column: "Login",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Clerks_PhoneNumber",
table: "Clerks",
column: "PhoneNumber",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Clients_ClerkId",
table: "Clients",
column: "ClerkId");
migrationBuilder.CreateIndex(
name: "IX_CreditProgramClients_CreditProgramId",
table: "CreditProgramClients",
column: "CreditProgramId");
migrationBuilder.CreateIndex(
name: "IX_CreditPrograms_Name",
table: "CreditPrograms",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_CreditPrograms_PeriodId",
table: "CreditPrograms",
column: "PeriodId");
migrationBuilder.CreateIndex(
name: "IX_CreditPrograms_StorekeeperId",
table: "CreditPrograms",
column: "StorekeeperId");
migrationBuilder.CreateIndex(
name: "IX_Currencies_Abbreviation",
table: "Currencies",
column: "Abbreviation",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Currencies_StorekeeperId",
table: "Currencies",
column: "StorekeeperId");
migrationBuilder.CreateIndex(
name: "IX_CurrencyCreditPrograms_CurrencyId",
table: "CurrencyCreditPrograms",
column: "CurrencyId");
migrationBuilder.CreateIndex(
name: "IX_DepositClients_ClientId",
table: "DepositClients",
column: "ClientId");
migrationBuilder.CreateIndex(
name: "IX_DepositCurrencies_CurrencyId",
table: "DepositCurrencies",
column: "CurrencyId");
migrationBuilder.CreateIndex(
name: "IX_Deposits_ClerkId",
table: "Deposits",
column: "ClerkId");
migrationBuilder.CreateIndex(
name: "IX_Periods_StorekeeperId",
table: "Periods",
column: "StorekeeperId");
migrationBuilder.CreateIndex(
name: "IX_Replenishments_ClerkId",
table: "Replenishments",
column: "ClerkId");
migrationBuilder.CreateIndex(
name: "IX_Replenishments_DepositId",
table: "Replenishments",
column: "DepositId");
migrationBuilder.CreateIndex(
name: "IX_Storekeepers_Email",
table: "Storekeepers",
column: "Email",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Storekeepers_Login",
table: "Storekeepers",
column: "Login",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Storekeepers_PhoneNumber",
table: "Storekeepers",
column: "PhoneNumber",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CreditProgramClients");
migrationBuilder.DropTable(
name: "CurrencyCreditPrograms");
migrationBuilder.DropTable(
name: "DepositClients");
migrationBuilder.DropTable(
name: "DepositCurrencies");
migrationBuilder.DropTable(
name: "Replenishments");
migrationBuilder.DropTable(
name: "CreditPrograms");
migrationBuilder.DropTable(
name: "Clients");
migrationBuilder.DropTable(
name: "Currencies");
migrationBuilder.DropTable(
name: "Deposits");
migrationBuilder.DropTable(
name: "Periods");
migrationBuilder.DropTable(
name: "Clerks");
migrationBuilder.DropTable(
name: "Storekeepers");
}
}
}

View File

@@ -0,0 +1,559 @@
// <auto-generated />
using System;
using BankDatabase;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace BankDatabase.Migrations
{
[DbContext(typeof(BankDbContext))]
partial class BankDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("BankDatabase.Models.Clerk", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Login")
.IsRequired()
.HasColumnType("text");
b.Property<string>("MiddleName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Surname")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Email")
.IsUnique();
b.HasIndex("Login")
.IsUnique();
b.HasIndex("PhoneNumber")
.IsUnique();
b.ToTable("Clerks");
});
modelBuilder.Entity("BankDatabase.Models.Client", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<decimal>("Balance")
.HasColumnType("numeric");
b.Property<string>("ClerkId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Surname")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("ClerkId");
b.ToTable("Clients");
});
modelBuilder.Entity("BankDatabase.Models.ClientCreditProgram", b =>
{
b.Property<string>("ClientId")
.HasColumnType("text");
b.Property<string>("CreditProgramId")
.HasColumnType("text");
b.HasKey("ClientId", "CreditProgramId");
b.HasIndex("CreditProgramId");
b.ToTable("CreditProgramClients");
});
modelBuilder.Entity("BankDatabase.Models.CreditProgram", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<decimal>("Cost")
.HasColumnType("numeric");
b.Property<decimal>("MaxCost")
.HasColumnType("numeric");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PeriodId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("StorekeeperId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.HasIndex("PeriodId");
b.HasIndex("StorekeeperId");
b.ToTable("CreditPrograms");
});
modelBuilder.Entity("BankDatabase.Models.CreditProgramCurrency", b =>
{
b.Property<string>("CreditProgramId")
.HasColumnType("text");
b.Property<string>("CurrencyId")
.HasColumnType("text");
b.HasKey("CreditProgramId", "CurrencyId");
b.HasIndex("CurrencyId");
b.ToTable("CurrencyCreditPrograms");
});
modelBuilder.Entity("BankDatabase.Models.Currency", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("Abbreviation")
.IsRequired()
.HasColumnType("text");
b.Property<decimal>("Cost")
.HasColumnType("numeric");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("StorekeeperId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Abbreviation")
.IsUnique();
b.HasIndex("StorekeeperId");
b.ToTable("Currencies");
});
modelBuilder.Entity("BankDatabase.Models.Deposit", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ClerkId")
.IsRequired()
.HasColumnType("text");
b.Property<decimal>("Cost")
.HasColumnType("numeric");
b.Property<float>("InterestRate")
.HasColumnType("real");
b.Property<int>("Period")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ClerkId");
b.ToTable("Deposits");
});
modelBuilder.Entity("BankDatabase.Models.DepositClient", b =>
{
b.Property<string>("DepositId")
.HasColumnType("text");
b.Property<string>("ClientId")
.HasColumnType("text");
b.HasKey("DepositId", "ClientId");
b.HasIndex("ClientId");
b.ToTable("DepositClients");
});
modelBuilder.Entity("BankDatabase.Models.DepositCurrency", b =>
{
b.Property<string>("DepositId")
.HasColumnType("text");
b.Property<string>("CurrencyId")
.HasColumnType("text");
b.HasKey("DepositId", "CurrencyId");
b.HasIndex("CurrencyId");
b.ToTable("DepositCurrencies");
});
modelBuilder.Entity("BankDatabase.Models.Period", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<DateTime>("EndTime")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("StartTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("StorekeeperId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("StorekeeperId");
b.ToTable("Periods");
});
modelBuilder.Entity("BankDatabase.Models.Replenishment", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<decimal>("Amount")
.HasColumnType("numeric");
b.Property<string>("ClerkId")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<string>("DepositId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("ClerkId");
b.HasIndex("DepositId");
b.ToTable("Replenishments");
});
modelBuilder.Entity("BankDatabase.Models.Storekeeper", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Login")
.IsRequired()
.HasColumnType("text");
b.Property<string>("MiddleName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Surname")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Email")
.IsUnique();
b.HasIndex("Login")
.IsUnique();
b.HasIndex("PhoneNumber")
.IsUnique();
b.ToTable("Storekeepers");
});
modelBuilder.Entity("BankDatabase.Models.Client", b =>
{
b.HasOne("BankDatabase.Models.Clerk", "Clerk")
.WithMany("Clients")
.HasForeignKey("ClerkId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Clerk");
});
modelBuilder.Entity("BankDatabase.Models.ClientCreditProgram", b =>
{
b.HasOne("BankDatabase.Models.Client", "Client")
.WithMany("CreditProgramClients")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("BankDatabase.Models.CreditProgram", "CreditProgram")
.WithMany("CreditProgramClients")
.HasForeignKey("CreditProgramId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
b.Navigation("CreditProgram");
});
modelBuilder.Entity("BankDatabase.Models.CreditProgram", b =>
{
b.HasOne("BankDatabase.Models.Period", "Period")
.WithMany("CreditPrograms")
.HasForeignKey("PeriodId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("BankDatabase.Models.Storekeeper", "Storekeeper")
.WithMany("CreditPrograms")
.HasForeignKey("StorekeeperId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Period");
b.Navigation("Storekeeper");
});
modelBuilder.Entity("BankDatabase.Models.CreditProgramCurrency", b =>
{
b.HasOne("BankDatabase.Models.CreditProgram", "CreditProgram")
.WithMany("CurrencyCreditPrograms")
.HasForeignKey("CreditProgramId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("BankDatabase.Models.Currency", "Currency")
.WithMany("CurrencyCreditPrograms")
.HasForeignKey("CurrencyId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("CreditProgram");
b.Navigation("Currency");
});
modelBuilder.Entity("BankDatabase.Models.Currency", b =>
{
b.HasOne("BankDatabase.Models.Storekeeper", "Storekeeper")
.WithMany("Currencies")
.HasForeignKey("StorekeeperId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Storekeeper");
});
modelBuilder.Entity("BankDatabase.Models.Deposit", b =>
{
b.HasOne("BankDatabase.Models.Clerk", "Clerk")
.WithMany("Deposits")
.HasForeignKey("ClerkId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Clerk");
});
modelBuilder.Entity("BankDatabase.Models.DepositClient", b =>
{
b.HasOne("BankDatabase.Models.Client", "Client")
.WithMany("DepositClients")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("BankDatabase.Models.Deposit", "Deposit")
.WithMany("DepositClients")
.HasForeignKey("DepositId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Client");
b.Navigation("Deposit");
});
modelBuilder.Entity("BankDatabase.Models.DepositCurrency", b =>
{
b.HasOne("BankDatabase.Models.Currency", "Currency")
.WithMany("DepositCurrencies")
.HasForeignKey("CurrencyId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("BankDatabase.Models.Deposit", "Deposit")
.WithMany("DepositCurrencies")
.HasForeignKey("DepositId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Currency");
b.Navigation("Deposit");
});
modelBuilder.Entity("BankDatabase.Models.Period", b =>
{
b.HasOne("BankDatabase.Models.Storekeeper", "Storekeeper")
.WithMany("Periods")
.HasForeignKey("StorekeeperId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Storekeeper");
});
modelBuilder.Entity("BankDatabase.Models.Replenishment", b =>
{
b.HasOne("BankDatabase.Models.Clerk", "Clerk")
.WithMany("Replenishments")
.HasForeignKey("ClerkId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("BankDatabase.Models.Deposit", "Deposit")
.WithMany("Replenishments")
.HasForeignKey("DepositId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Clerk");
b.Navigation("Deposit");
});
modelBuilder.Entity("BankDatabase.Models.Clerk", b =>
{
b.Navigation("Clients");
b.Navigation("Deposits");
b.Navigation("Replenishments");
});
modelBuilder.Entity("BankDatabase.Models.Client", b =>
{
b.Navigation("CreditProgramClients");
b.Navigation("DepositClients");
});
modelBuilder.Entity("BankDatabase.Models.CreditProgram", b =>
{
b.Navigation("CreditProgramClients");
b.Navigation("CurrencyCreditPrograms");
});
modelBuilder.Entity("BankDatabase.Models.Currency", b =>
{
b.Navigation("CurrencyCreditPrograms");
b.Navigation("DepositCurrencies");
});
modelBuilder.Entity("BankDatabase.Models.Deposit", b =>
{
b.Navigation("DepositClients");
b.Navigation("DepositCurrencies");
b.Navigation("Replenishments");
});
modelBuilder.Entity("BankDatabase.Models.Period", b =>
{
b.Navigation("CreditPrograms");
});
modelBuilder.Entity("BankDatabase.Models.Storekeeper", b =>
{
b.Navigation("CreditPrograms");
b.Navigation("Currencies");
b.Navigation("Periods");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -2,7 +2,7 @@
namespace BankDatabase.Models;
class Clerk
public class Clerk
{
public required string Id { get; set; }

View File

@@ -2,7 +2,7 @@
namespace BankDatabase.Models;
class Client
public class Client
{
public required string Id { get; set; }

View File

@@ -1,6 +1,8 @@
namespace BankDatabase.Models;
using System.ComponentModel.DataAnnotations.Schema;
class ClientCreditProgram
namespace BankDatabase.Models;
public class ClientCreditProgram
{
public required string ClientId { get; set; }

View File

@@ -2,7 +2,7 @@
namespace BankDatabase.Models;
class CreditProgram
public class CreditProgram
{
public required string Id { get; set; }

View File

@@ -1,6 +1,6 @@
namespace BankDatabase.Models;
class CreditProgramCurrency
public class CreditProgramCurrency
{
public required string CreditProgramId { get; set; }

View File

@@ -2,7 +2,7 @@
namespace BankDatabase.Models;
class Currency
public class Currency
{
public required string Id { get; set; }

View File

@@ -2,7 +2,7 @@
namespace BankDatabase.Models;
class Deposit
public class Deposit
{
public required string Id { get; set; }

View File

@@ -1,6 +1,8 @@
namespace BankDatabase.Models;
using System.ComponentModel.DataAnnotations.Schema;
class DepositClient
namespace BankDatabase.Models;
public class DepositClient
{
public required string DepositId { get; set; }

View File

@@ -1,6 +1,8 @@
namespace BankDatabase.Models;
using System.ComponentModel.DataAnnotations.Schema;
class DepositCurrency
namespace BankDatabase.Models;
public class DepositCurrency
{
public required string DepositId { get; set; }

View File

@@ -2,7 +2,7 @@
namespace BankDatabase.Models;
class Period
public class Period
{
public required string Id { get; set; }

View File

@@ -2,7 +2,7 @@
namespace BankDatabase.Models;
class Replenishment
public class Replenishment
{
public required string Id { get; set; }

View File

@@ -2,7 +2,7 @@
namespace BankDatabase.Models;
class Storekeeper
public class Storekeeper
{
public required string Id { get; set; }

View File

@@ -5,5 +5,5 @@ namespace BankTests.Infrastructure;
internal class ConfigurationDatabase : IConfigurationDatabase
{
public string ConnectionString =>
"Host=127.0.0.1;Port=5432;Database=TitanicTest;Username=postgres;Password=postgres;Include Error Detail=true";
"Host=127.0.0.1;Port=5432;Database=TitanicTest;Username=postgres;Password=admin123;Include Error Detail=true";
}

View File

@@ -10,7 +10,7 @@ internal class BaseStorageContractTest
[OneTimeSetUp]
public void OneTimeSetUp()
{
BankDbContext = new BankDbContext(new ConfigurationDatabase());
BankDbContext = new BankDbContext(new Infrastructure.ConfigurationDatabase());
BankDbContext.Database.EnsureDeleted();
BankDbContext.Database.EnsureCreated();

View File

@@ -71,6 +71,186 @@ internal class CreditProgramStorageContractTests : BaseStorageContractTest
Assert.That(list, Is.Empty);
}
[Test]
public void Try_GetList_WithCurrencyRelations_Test()
{
// Создаем storekeeper и сохраняем его
var uniqueId = Guid.NewGuid();
var storekeeper = BankDbContext.InsertStorekeeperToDatabaseAndReturn(
login: $"storekeeper_{uniqueId}",
email: $"storekeeper_{uniqueId}@email.com",
phone: $"+7-777-777-{uniqueId.ToString().Substring(0, 4)}"
);
BankDbContext.SaveChanges();
// Проверяем, что storekeeper действительно сохранен
var savedStorekeeper = BankDbContext.Storekeepers.FirstOrDefault(s => s.Id == storekeeper.Id);
Assert.That(savedStorekeeper, Is.Not.Null, "Storekeeper не был сохранен в базе данных");
var storekeeperId = savedStorekeeper.Id;
// Создаем несколько валют
var currency1Id = BankDbContext.InsertCurrencyToDatabaseAndReturn(storekeeperId: storekeeperId, abbreviation: "USD").Id;
var currency2Id = BankDbContext.InsertCurrencyToDatabaseAndReturn(storekeeperId: storekeeperId, abbreviation: "EUR").Id;
// Создаем кредитную программу с двумя валютами
var creditProgram = BankDbContext.InsertCreditProgramToDatabaseAndReturn(
storekeeperId: storekeeperId,
periodId: _periodId,
creditProgramCurrency: [
(currency1Id, Guid.NewGuid().ToString()),
(currency2Id, Guid.NewGuid().ToString())
]
);
var list = _storageContract.GetList();
Assert.That(list, Is.Not.Null);
Assert.That(list, Has.Count.EqualTo(1));
var result = list.First();
Assert.That(result.Currencies, Is.Not.Null);
Assert.That(result.Currencies, Has.Count.EqualTo(2));
Assert.That(result.Currencies.Select(c => c.CurrencyId), Does.Contain(currency1Id));
Assert.That(result.Currencies.Select(c => c.CurrencyId), Does.Contain(currency2Id));
}
[Test]
public void Try_AddElement_WithCurrencyRelations_Test()
{
// Создаем storekeeper и сохраняем его
var uniqueId = Guid.NewGuid();
var storekeeper = BankDbContext.InsertStorekeeperToDatabaseAndReturn(
login: $"storekeeper_{uniqueId}",
email: $"storekeeper_{uniqueId}@email.com",
phone: $"+7-777-777-{uniqueId.ToString().Substring(0, 4)}"
);
BankDbContext.SaveChanges();
// Проверяем, что storekeeper действительно сохранен
var savedStorekeeper = BankDbContext.Storekeepers.FirstOrDefault(s => s.Id == storekeeper.Id);
Assert.That(savedStorekeeper, Is.Not.Null, "Storekeeper не был сохранен в базе данных");
var storekeeperId = savedStorekeeper.Id;
// Создаем валюту
var currencyId = BankDbContext.InsertCurrencyToDatabaseAndReturn(storekeeperId: storekeeperId).Id;
// Создаем модель с валютой
var creditProgram = CreateModel(
name: "unique name",
periodId: _periodId,
storekeeperId: storekeeperId,
currency: [
new CreditProgramCurrencyDataModel(Guid.NewGuid().ToString(), currencyId)
]
);
_storageContract.AddElement(creditProgram);
var result = BankDbContext.GetCreditProgramFromDatabase(creditProgram.Id);
Assert.That(result, Is.Not.Null);
Assert.That(result.CurrencyCreditPrograms, Is.Not.Null);
Assert.That(result.CurrencyCreditPrograms, Has.Count.EqualTo(1));
Assert.That(result.CurrencyCreditPrograms.First().CurrencyId, Is.EqualTo(currencyId));
}
[Test]
public void Try_UpdElement_WithCurrencyRelations_Test()
{
// Создаем storekeeper и сохраняем его
var uniqueId = Guid.NewGuid();
var storekeeper = BankDbContext.InsertStorekeeperToDatabaseAndReturn(
login: $"storekeeper_{uniqueId}",
email: $"storekeeper_{uniqueId}@email.com",
phone: $"+7-777-777-{uniqueId.ToString().Substring(0, 4)}"
);
BankDbContext.SaveChanges();
// Проверяем, что storekeeper действительно сохранен
var savedStorekeeper = BankDbContext.Storekeepers.FirstOrDefault(s => s.Id == storekeeper.Id);
Assert.That(savedStorekeeper, Is.Not.Null, "Storekeeper не был сохранен в базе данных");
var storekeeperId = savedStorekeeper.Id;
// Создаем две валюты
var currency1Id = BankDbContext.InsertCurrencyToDatabaseAndReturn(storekeeperId: storekeeperId).Id;
var currency2Id = BankDbContext.InsertCurrencyToDatabaseAndReturn(storekeeperId: storekeeperId).Id;
// Создаем кредитную программу с одной валютой
var creditProgram = BankDbContext.InsertCreditProgramToDatabaseAndReturn(
storekeeperId: storekeeperId,
periodId: _periodId,
creditProgramCurrency: [(currency1Id, Guid.NewGuid().ToString())]
);
// Обновляем программу, добавляя вторую валюту
var updatedModel = CreateModel(
id: creditProgram.Id,
name: creditProgram.Name,
periodId: _periodId,
storekeeperId: storekeeperId,
currency: [
new CreditProgramCurrencyDataModel(creditProgram.Id, currency1Id),
new CreditProgramCurrencyDataModel(creditProgram.Id, currency2Id)
]
);
_storageContract.UpdElement(updatedModel);
var result = BankDbContext.GetCreditProgramFromDatabase(creditProgram.Id);
Assert.That(result, Is.Not.Null);
Assert.That(result.CurrencyCreditPrograms, Is.Not.Null);
Assert.That(result.CurrencyCreditPrograms, Has.Count.EqualTo(2));
Assert.That(result.CurrencyCreditPrograms.Select(c => c.CurrencyId), Does.Contain(currency1Id));
Assert.That(result.CurrencyCreditPrograms.Select(c => c.CurrencyId), Does.Contain(currency2Id));
}
[Test]
public void Try_UpdElement_RemoveCurrencyRelations_Test()
{
// Создаем storekeeper и сохраняем его
var uniqueId = Guid.NewGuid();
var storekeeper = BankDbContext.InsertStorekeeperToDatabaseAndReturn(
login: $"storekeeper_{uniqueId}",
email: $"storekeeper_{uniqueId}@email.com",
phone: $"+7-777-777-{uniqueId.ToString().Substring(0, 4)}"
);
BankDbContext.SaveChanges();
// Проверяем, что storekeeper действительно сохранен
var savedStorekeeper = BankDbContext.Storekeepers.FirstOrDefault(s => s.Id == storekeeper.Id);
Assert.That(savedStorekeeper, Is.Not.Null, "Storekeeper не был сохранен в базе данных");
var storekeeperId = savedStorekeeper.Id;
// Создаем две валюты
var currency1Id = BankDbContext.InsertCurrencyToDatabaseAndReturn(storekeeperId: storekeeperId).Id;
var currency2Id = BankDbContext.InsertCurrencyToDatabaseAndReturn(storekeeperId: storekeeperId).Id;
// Создаем кредитную программу с двумя валютами
var creditProgram = BankDbContext.InsertCreditProgramToDatabaseAndReturn(
storekeeperId: storekeeperId,
periodId: _periodId,
creditProgramCurrency: [
(currency1Id, Guid.NewGuid().ToString()),
(currency2Id, Guid.NewGuid().ToString())
]
);
// Обновляем программу, оставляя только одну валюту
var updatedModel = CreateModel(
id: creditProgram.Id,
name: creditProgram.Name,
periodId: _periodId,
storekeeperId: storekeeperId,
currency: [new CreditProgramCurrencyDataModel(creditProgram.Id, currency1Id)]
);
_storageContract.UpdElement(updatedModel);
var result = BankDbContext.GetCreditProgramFromDatabase(creditProgram.Id);
Assert.That(result, Is.Not.Null);
Assert.That(result.CurrencyCreditPrograms, Is.Not.Null);
Assert.That(result.CurrencyCreditPrograms, Has.Count.EqualTo(1));
Assert.That(result.CurrencyCreditPrograms.First().CurrencyId, Is.EqualTo(currency1Id));
}
[Test]
public void Try_GetElementById_WhenHaveRecord_Test()
{

View File

@@ -18,7 +18,12 @@ internal class DepositStorageContractTests : BaseStorageContractTest
public void SetUp()
{
_storageContract = new DepositStorageContract(BankDbContext);
_clerkId = BankDbContext.InsertClerkToDatabaseAndReturn().Id;
var uniqueId = Guid.NewGuid();
_clerkId = BankDbContext.InsertClerkToDatabaseAndReturn(
login: $"clerk_{uniqueId}",
email: $"clerk_{uniqueId}@email.com",
phone: $"+7-777-777-{uniqueId.ToString().Substring(0, 4)}"
).Id;
}
[TearDown]
@@ -93,6 +98,131 @@ internal class DepositStorageContractTests : BaseStorageContractTest
);
}
[Test]
public void Try_GetList_ByClerkId_Test()
{
var uniqueId1 = Guid.NewGuid();
var uniqueId2 = Guid.NewGuid();
var clerkId1 = BankDbContext.InsertClerkToDatabaseAndReturn(
email: $"clerk1_{uniqueId1}@email.com",
login: $"clerk1_{uniqueId1}",
phone: $"+7-777-777-{uniqueId1.ToString().Substring(0, 4)}"
).Id;
var clerkId2 = BankDbContext.InsertClerkToDatabaseAndReturn(
email: $"clerk2_{uniqueId2}@email.com",
login: $"clerk2_{uniqueId2}",
phone: $"+7-777-777-{uniqueId2.ToString().Substring(0, 4)}"
).Id;
BankDbContext.InsertDepositToDatabaseAndReturn(clerkId: clerkId1);
BankDbContext.InsertDepositToDatabaseAndReturn(clerkId: clerkId1);
BankDbContext.InsertDepositToDatabaseAndReturn(clerkId: clerkId2);
var list = _storageContract.GetList(clerkId1);
Assert.That(list, Is.Not.Null);
Assert.That(list, Has.Count.EqualTo(2));
Assert.That(list.All(x => x.ClerkId == clerkId1), Is.True);
}
[Test]
public void Try_GetElementByInterestRate_WhenHaveRecord_Test()
{
var deposit = BankDbContext.InsertDepositToDatabaseAndReturn(clerkId: _clerkId, interestRate: 15.5f);
var result = _storageContract.GetElementByInterestRate(15.5f);
Assert.That(result, Is.Not.Null);
AssertElement(result, deposit);
}
[Test]
public void Try_GetElementByInterestRate_WhenNoRecord_Test()
{
var result = _storageContract.GetElementByInterestRate(15.5f);
Assert.That(result, Is.Null);
}
[Test]
public void Try_AddElement_WhenHaveRecordWithSameInterestRate_Test()
{
// Создаем первый депозит с определенной процентной ставкой
var deposit1 = CreateModel(clerkId: _clerkId, interestRate: 10.5f);
_storageContract.AddElement(deposit1);
// Создаем второй депозит с такой же процентной ставкой
var deposit2 = CreateModel(clerkId: _clerkId, interestRate: 10.5f);
// Проверяем, что можно добавить депозит с такой же процентной ставкой
_storageContract.AddElement(deposit2);
var result = BankDbContext.GetDepositFromDatabase(deposit2.Id);
Assert.That(result, Is.Not.Null);
AssertElement(result, deposit2);
}
[Test]
public void Try_UpdElement_WithCurrencies_Test()
{
// Создаем валюты
var storekeeperId = BankDbContext.InsertStorekeeperToDatabaseAndReturn(
login: $"storekeeper_{Guid.NewGuid()}",
email: $"storekeeper_{Guid.NewGuid()}@email.com",
phone: $"+7-777-777-{Guid.NewGuid().ToString().Substring(0, 4)}"
).Id;
var currency1 = BankDbContext.InsertCurrencyToDatabaseAndReturn(
abbreviation: "USD",
storekeeperId: storekeeperId
);
var currency2 = BankDbContext.InsertCurrencyToDatabaseAndReturn(
abbreviation: "EUR",
storekeeperId: storekeeperId
);
// Создаем депозит
var deposit = BankDbContext.InsertDepositToDatabaseAndReturn(clerkId: _clerkId);
// Обновляем депозит с валютами
var updatedDeposit = CreateModel(
id: deposit.Id,
clerkId: _clerkId,
deposits: new List<DepositCurrencyDataModel>
{
new(deposit.Id, currency1.Id),
new(deposit.Id, currency2.Id)
}
);
_storageContract.UpdElement(updatedDeposit);
var result = BankDbContext.GetDepositFromDatabase(deposit.Id);
Assert.That(result.DepositCurrencies, Has.Count.EqualTo(2));
}
[Test]
public async Task Try_GetListAsync_ByDateRange_Test()
{
var startDate = DateTime.Now.AddDays(-1);
var endDate = DateTime.Now.AddDays(1);
var deposit = BankDbContext.InsertDepositToDatabaseAndReturn(clerkId: _clerkId);
var list = await _storageContract.GetListAsync(startDate, endDate, CancellationToken.None);
Assert.That(list, Is.Not.Null);
Assert.That(list, Has.Count.EqualTo(1));
AssertElement(list.First(), deposit);
}
[Test]
public void Try_AddElement_WhenDatabaseError_Test()
{
var deposit = CreateModel(clerkId: _clerkId);
// Симулируем ошибку базы данных, пытаясь добавить депозит с несуществующим ID клерка
var nonExistentClerkId = Guid.NewGuid().ToString();
var depositWithInvalidClerk = CreateModel(clerkId: nonExistentClerkId);
Assert.That(
() => _storageContract.AddElement(depositWithInvalidClerk),
Throws.TypeOf<StorageException>()
);
}
private static DepositDataModel CreateModel(
string? id = null,
float interestRate = 10,

View File

@@ -20,7 +20,12 @@ internal class ReplenishmentStorageContractTests : BaseStorageContractTest
public void SetUp()
{
_storageContract = new ReplenishmentStorageContract(BankDbContext);
_clerkId = BankDbContext.InsertClerkToDatabaseAndReturn().Id;
var uniqueId = Guid.NewGuid();
_clerkId = BankDbContext.InsertClerkToDatabaseAndReturn(
login: $"clerk_{uniqueId}",
email: $"clerk_{uniqueId}@email.com",
phone: $"+7-777-777-{uniqueId.ToString().Substring(0, 4)}"
).Id;
_depositId = BankDbContext.InsertDepositToDatabaseAndReturn(clerkId: _clerkId).Id;
}
@@ -62,6 +67,120 @@ internal class ReplenishmentStorageContractTests : BaseStorageContractTest
Assert.That(list, Is.Empty);
}
[Test]
public void Try_GetList_WithDateFilters_Test()
{
var now = DateTime.UtcNow;
var pastDate = now.AddDays(-1);
var futureDate = now.AddDays(1);
// Insert replenishments with different dates
var pastReplenishment = BankDbContext.InsertReplenishmentToDatabaseAndReturn(
clerkId: _clerkId,
depositId: _depositId,
date: pastDate
);
var currentReplenishment = BankDbContext.InsertReplenishmentToDatabaseAndReturn(
clerkId: _clerkId,
depositId: _depositId,
date: now
);
var futureReplenishment = BankDbContext.InsertReplenishmentToDatabaseAndReturn(
clerkId: _clerkId,
depositId: _depositId,
date: futureDate
);
// Test date range filter
var filteredList = _storageContract.GetList(fromDate: pastDate, toDate: now);
Assert.That(filteredList, Has.Count.EqualTo(2));
Assert.That(filteredList.Select(x => x.Id), Does.Contain(pastReplenishment.Id));
Assert.That(filteredList.Select(x => x.Id), Does.Contain(currentReplenishment.Id));
Assert.That(filteredList.Select(x => x.Id), Does.Not.Contain(futureReplenishment.Id));
}
[Test]
public void Try_GetList_WithClerkIdFilter_Test()
{
var otherClerkId = BankDbContext.InsertClerkToDatabaseAndReturn(
login: $"clerk_other",
email: "clerk_other@email.com",
phone: "+7-777-777-0000"
).Id;
// Insert replenishments for different clerks
var replenishment1 = BankDbContext.InsertReplenishmentToDatabaseAndReturn(
clerkId: _clerkId,
depositId: _depositId
);
var replenishment2 = BankDbContext.InsertReplenishmentToDatabaseAndReturn(
clerkId: otherClerkId,
depositId: _depositId
);
var filteredList = _storageContract.GetList(clerkId: _clerkId);
Assert.That(filteredList, Has.Count.EqualTo(1));
Assert.That(filteredList.First().Id, Is.EqualTo(replenishment1.Id));
}
[Test]
public void Try_GetList_WithDepositIdFilter_Test()
{
var otherDepositId = BankDbContext.InsertDepositToDatabaseAndReturn(clerkId: _clerkId).Id;
// Insert replenishments for different deposits
var replenishment1 = BankDbContext.InsertReplenishmentToDatabaseAndReturn(
clerkId: _clerkId,
depositId: _depositId
);
var replenishment2 = BankDbContext.InsertReplenishmentToDatabaseAndReturn(
clerkId: _clerkId,
depositId: otherDepositId
);
var filteredList = _storageContract.GetList(depositId: _depositId);
Assert.That(filteredList, Has.Count.EqualTo(1));
Assert.That(filteredList.First().Id, Is.EqualTo(replenishment1.Id));
}
[Test]
public void Try_GetList_WithCombinedFilters_Test()
{
var now = DateTime.UtcNow;
var otherClerkId = BankDbContext.InsertClerkToDatabaseAndReturn(
login: $"clerk_other",
email: "clerk_other@email.com",
phone: "+7-777-777-0000"
).Id;
var otherDepositId = BankDbContext.InsertDepositToDatabaseAndReturn(clerkId: _clerkId).Id;
// Insert replenishments with different combinations
var replenishment1 = BankDbContext.InsertReplenishmentToDatabaseAndReturn(
clerkId: _clerkId,
depositId: _depositId,
date: now
);
var replenishment2 = BankDbContext.InsertReplenishmentToDatabaseAndReturn(
clerkId: otherClerkId,
depositId: _depositId,
date: now
);
var replenishment3 = BankDbContext.InsertReplenishmentToDatabaseAndReturn(
clerkId: _clerkId,
depositId: otherDepositId,
date: now
);
var filteredList = _storageContract.GetList(
fromDate: now,
toDate: now,
clerkId: _clerkId,
depositId: _depositId
);
Assert.That(filteredList, Has.Count.EqualTo(1));
Assert.That(filteredList.First().Id, Is.EqualTo(replenishment1.Id));
}
[Test]
public void Try_GetElementById_WhenHaveRecord_Test()
{

View File

@@ -72,7 +72,7 @@ internal class ClerkControllerTests : BaseWebApiControllerTest
// Arrange
var model = CreateModel();
// Act
var response = await HttpClient.PostAsJsonAsync("/api/clerks", model);
var response = await HttpClient.PostAsJsonAsync("/api/clerks/register", model);
// Assert
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.NoContent));
AssertElement(BankDbContext.GetClerkFromDatabase(model.Id!), model);
@@ -85,7 +85,7 @@ internal class ClerkControllerTests : BaseWebApiControllerTest
var model = CreateModel();
BankDbContext.InsertClerkToDatabaseAndReturn(id: model.Id);
// Act
var response = await HttpClient.PostAsJsonAsync("/api/clerks", model);
var response = await HttpClient.PostAsJsonAsync("/api/clerks/register", model);
// Assert
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
}
@@ -94,7 +94,7 @@ internal class ClerkControllerTests : BaseWebApiControllerTest
public async Task Post_WhenSendEmptyData_ShouldBadRequest_Test()
{
// Act
var response = await HttpClient.PostAsync("/api/clerks", MakeContent(string.Empty));
var response = await HttpClient.PostAsync("/api/clerks/register", MakeContent(string.Empty));
// Assert
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
}

View File

@@ -34,7 +34,7 @@ internal class ClientControllerTests : BaseWebApiControllerTest
var client1 = BankDbContext.InsertClientToDatabaseAndReturn(name: "Иван", surname: "Иванов", clerkId: _clerk.Id);
var client2 = BankDbContext.InsertClientToDatabaseAndReturn(name: "Петр", surname: "Петров", clerkId: _clerk.Id);
// Act
var response = await HttpClient.GetAsync("/api/clients/getrecords");
var response = await HttpClient.GetAsync("/api/clients/getallrecords");
// Assert
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var data = await GetModelFromResponseAsync<List<ClientViewModel>>(response);
@@ -48,7 +48,7 @@ internal class ClientControllerTests : BaseWebApiControllerTest
public async Task GetList_WhenNoRecords_ShouldSuccess_Test()
{
// Act
var response = await HttpClient.GetAsync("/api/clients/getrecords");
var response = await HttpClient.GetAsync("/api/clients/getallrecords");
// Assert
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var data = await GetModelFromResponseAsync<List<ClientViewModel>>(response);
@@ -81,7 +81,7 @@ internal class ClientControllerTests : BaseWebApiControllerTest
public async Task Post_ShouldSuccess_Test()
{
// Arrange
var model = CreateModel();
var model = CreateModel(clerkId: _clerk.Id);
// Act
var response = await HttpClient.PostAsJsonAsync("/api/clients/register", model);
// Assert
@@ -114,7 +114,7 @@ internal class ClientControllerTests : BaseWebApiControllerTest
public async Task Put_ShouldSuccess_Test()
{
// Arrange
var model = CreateModel();
var model = CreateModel(name: "slava", surname: "fomichev", balance: 1_000_000, clerkId: _clerk.Id);
BankDbContext.InsertClientToDatabaseAndReturn(id: model.Id, clerkId: _clerk.Id);
// Act
var response = await HttpClient.PutAsJsonAsync("/api/clients/changeinfo", model);

View File

@@ -1,4 +1,5 @@
using AutoMapper;
using BankBusinessLogic.Implementations;
using BankContracts.AdapterContracts;
using BankContracts.AdapterContracts.OperationResponses;
using BankContracts.BindingModels;
@@ -6,6 +7,7 @@ using BankContracts.BusinessLogicContracts;
using BankContracts.DataModels;
using BankContracts.Exceptions;
using BankContracts.ViewModels;
using BankWebApi.Infrastructure;
namespace BankWebApi.Adapters;
@@ -13,11 +15,13 @@ public class ClerkAdapter : IClerkAdapter
{
private readonly IClerkBusinessLogicContract _clerkBusinessLogicContract;
private readonly IJwtProvider _jwtProvider;
private readonly ILogger _logger;
private readonly Mapper _mapper;
public ClerkAdapter(IClerkBusinessLogicContract clerkBusinessLogicContract, ILogger logger)
public ClerkAdapter(IClerkBusinessLogicContract clerkBusinessLogicContract, ILogger logger, IJwtProvider jwtProvider)
{
_clerkBusinessLogicContract = clerkBusinessLogicContract;
_logger = logger;
@@ -27,6 +31,7 @@ public class ClerkAdapter : IClerkAdapter
cfg.CreateMap<ClerkDataModel, ClerkViewModel>();
});
_mapper = new Mapper(config);
_jwtProvider = jwtProvider;
}
public ClerkOperationResponse GetList()
@@ -170,4 +175,30 @@ public class ClerkAdapter : IClerkAdapter
return ClerkOperationResponse.InternalServerError(ex.Message);
}
}
public ClerkOperationResponse Login(LoginBindingModel clerkModel, out string token)
{
token = string.Empty;
try
{
var clerk = _clerkBusinessLogicContract.GetClerkByData(clerkModel.Login);
var result = clerkModel.Password == clerk.Password;
if (!result)
{
return ClerkOperationResponse.Unauthorized("Password are incorrect");
}
token = _jwtProvider.GenerateToken(clerk);
return ClerkOperationResponse.OK(_mapper.Map<ClerkViewModel>(clerk));
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception in Login");
return ClerkOperationResponse.InternalServerError($"Exception in Login {ex.Message}");
}
}
}

View File

@@ -24,13 +24,25 @@ public class ClientAdapter : IClientAdapter
_logger = logger;
var config = new MapperConfiguration(cfg =>
{
// Mapping for Client
cfg.CreateMap<ClientBindingModel, ClientDataModel>();
cfg.CreateMap<DepositDataModel, DepositViewModel>();
cfg.CreateMap<ClientDataModel, ClientViewModel>()
.ForMember(dest => dest.DepositClients, opt => opt.MapFrom(src => src.DepositClients))
.ForMember(dest => dest.CreditProgramClients, opt => opt.MapFrom(src => src.CreditProgramClients));
// Mapping for Deposit
cfg.CreateMap<DepositDataModel, DepositViewModel>()
.ForMember(dest => dest.DepositCurrencies, opt => opt.MapFrom(src => src.Currencies)); // Adjust if Currencies is meant to map to DepositClients
// Mapping for ClientCreditProgram
cfg.CreateMap<ClientCreditProgramBindingModel, ClientCreditProgramDataModel>();
cfg.CreateMap<ClientCreditProgramDataModel, ClientCreditProgramViewModel>();
// Mapping for DepositClient
cfg.CreateMap<DepositClientBindingModel, DepositClientDataModel>();
cfg.CreateMap<DepositClientDataModel, DepositClientViewModel>();
});
_mapper = new Mapper(config);
}

View File

@@ -54,7 +54,7 @@ public class CreditProgramAdapter : ICreditProgramAdapter
{
_logger.LogError(ex, "StorageException");
return CreditProgramOperationResponse.InternalServerError(
$"Error while working with data storage:{ex.InnerException!.Message}"
$"Error while working with data storage:{ex.InnerException?.Message}"
);
}
catch (Exception ex)
@@ -86,7 +86,7 @@ public class CreditProgramAdapter : ICreditProgramAdapter
{
_logger.LogError(ex, "StorageException");
return CreditProgramOperationResponse.InternalServerError(
$"Error while working with data storage: {ex.InnerException!.Message}"
$"Error while working with data storage: {ex.InnerException?.Message}"
);
}
catch (Exception ex)
@@ -122,7 +122,7 @@ public class CreditProgramAdapter : ICreditProgramAdapter
{
_logger.LogError(ex, "StorageException");
return CreditProgramOperationResponse.BadRequest(
$"Error while working with data storage: {ex.InnerException!.Message}"
$"Error while working with data storage: {ex.InnerException?.Message}"
);
}
catch (Exception ex)
@@ -164,7 +164,7 @@ public class CreditProgramAdapter : ICreditProgramAdapter
{
_logger.LogError(ex, "StorageException");
return CreditProgramOperationResponse.BadRequest(
$"Error while working with data storage: {ex.InnerException!.Message}"
$"Error while working with data storage: {ex.InnerException?.Message}"
);
}
catch (Exception ex)
@@ -195,7 +195,7 @@ public class CreditProgramAdapter : ICreditProgramAdapter
{
_logger.LogError(ex, "StorageException");
return CreditProgramOperationResponse.InternalServerError(
$"Error while working with data storage:{ex.InnerException!.Message}"
$"Error while working with data storage:{ex.InnerException?.Message}"
);
}
catch (Exception ex)
@@ -226,7 +226,7 @@ public class CreditProgramAdapter : ICreditProgramAdapter
{
_logger.LogError(ex, "StorageException");
return CreditProgramOperationResponse.InternalServerError(
$"Error while working with data storage:{ex.InnerException!.Message}"
$"Error while working with data storage:{ex.InnerException?.Message}"
);
}
catch (Exception ex)

View File

@@ -23,10 +23,34 @@ public class DepositAdapter : IDepositAdapter
_logger = logger;
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<DepositBindingModel, DepositDataModel>();
cfg.CreateMap<DepositDataModel, DepositViewModel>();
cfg.CreateMap<DepositCurrencyBindingModel, DepositCurrencyDataModel>();
// DepositBindingModel -> DepositDataModel
cfg.CreateMap<DepositBindingModel, DepositDataModel>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id ?? string.Empty))
.ForMember(dest => dest.InterestRate, opt => opt.MapFrom(src => src.InterestRate))
.ForMember(dest => dest.Cost, opt => opt.MapFrom(src => src.Cost))
.ForMember(dest => dest.Period, opt => opt.MapFrom(src => src.Period))
.ForMember(dest => dest.ClerkId, opt => opt.MapFrom(src => src.ClerkId ?? string.Empty))
.ForMember(dest => dest.Currencies, opt => opt.MapFrom(src => src.DepositCurrencies ?? new List<DepositCurrencyBindingModel>()));
// DepositDataModel -> DepositViewModel
cfg.CreateMap<DepositDataModel, DepositViewModel>()
.ForMember(dest => dest.DepositCurrencies, opt => opt.MapFrom(src => src.Currencies != null ? src.Currencies : new List<DepositCurrencyDataModel>()));
// DepositCurrencyBindingModel -> DepositCurrencyDataModel
cfg.CreateMap<DepositCurrencyBindingModel, DepositCurrencyDataModel>()
.ForMember(dest => dest.DepositId, opt => opt.MapFrom(src => src.DepositId ?? string.Empty))
.ForMember(dest => dest.CurrencyId, opt => opt.MapFrom(src => src.CurrencyId ?? string.Empty));
// DepositCurrencyDataModel -> DepositCurrencyViewModel
cfg.CreateMap<DepositCurrencyDataModel, DepositCurrencyViewModel>();
// DepositCurrencyViewModel -> DepositCurrencyBindingModel
cfg.CreateMap<DepositCurrencyViewModel, DepositCurrencyBindingModel>();
// Явный маппинг DepositCurrencyDataModel -> DepositCurrencyBindingModel
cfg.CreateMap<DepositCurrencyDataModel, DepositCurrencyBindingModel>()
.ForMember(dest => dest.DepositId, opt => opt.MapFrom(src => src.DepositId))
.ForMember(dest => dest.CurrencyId, opt => opt.MapFrom(src => src.CurrencyId));
});
_mapper = new Mapper(config);
}
@@ -117,7 +141,7 @@ public class DepositAdapter : IDepositAdapter
{
_logger.LogError(ex, "StorageException");
return DepositOperationResponse.BadRequest(
$"Error while working with data storage: {ex.InnerException!.Message}"
$"Error while working with data storage: {ex.InnerException?.Message}"
);
}
catch (Exception ex)

View File

@@ -21,9 +21,17 @@ public class PeriodAdapter : IPeriodAdapter
{
_periodBusinessLogicContract = periodBusinessLogicContract;
_logger = logger;
var config = new MapperConfiguration(cfg =>
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<PeriodBindingModel, PeriodDataModel>();
cfg.CreateMap<PeriodBindingModel, PeriodDataModel>()
.ConstructUsing(src => new PeriodDataModel(
src.Id,
src.StartTime,
src.EndTime,
src.StorekeeperId
));
// Маппинг PeriodDataModel -> PeriodViewModel
cfg.CreateMap<PeriodDataModel, PeriodViewModel>();
});
_mapper = new Mapper(config);

View File

@@ -7,6 +7,8 @@ using BankContracts.BusinessLogicContracts;
using BankContracts.DataModels;
using BankContracts.Exceptions;
using BankContracts.ViewModels;
using BankWebApi.Infrastructure;
using System.Buffers;
namespace BankWebApi.Adapters;
@@ -14,11 +16,13 @@ public class StorekeeperAdapter : IStorekeeperAdapter
{
private readonly IStorekeeperBusinessLogicContract _storekeeperBusinessLogicContract;
private readonly IJwtProvider _jwtProvider;
private readonly ILogger _logger;
private readonly Mapper _mapper;
public StorekeeperAdapter(IStorekeeperBusinessLogicContract storekeeperBusinessLogicContract, ILogger logger)
public StorekeeperAdapter(IStorekeeperBusinessLogicContract storekeeperBusinessLogicContract, IJwtProvider jwtProvider, ILogger logger)
{
_storekeeperBusinessLogicContract = storekeeperBusinessLogicContract;
_logger = logger;
@@ -28,6 +32,7 @@ public class StorekeeperAdapter : IStorekeeperAdapter
cfg.CreateMap<StorekeeperDataModel, StorekeeperViewModel>();
});
_mapper = new Mapper(config);
_jwtProvider = jwtProvider;
}
public StorekeeperOperationResponse GetList()
@@ -119,7 +124,7 @@ public class StorekeeperAdapter : IStorekeeperAdapter
{
_logger.LogError(ex, "StorageException");
return StorekeeperOperationResponse.BadRequest(
$"Error while working with data storage: {ex.InnerException!.Message}"
$"Error while working with data storage: {ex.InnerException?.Message}"
);
}
catch (Exception ex)
@@ -170,5 +175,31 @@ public class StorekeeperAdapter : IStorekeeperAdapter
_logger.LogError(ex, "Exception");
return StorekeeperOperationResponse.InternalServerError(ex.Message);
}
}
}
public StorekeeperOperationResponse Login(LoginBindingModel storekeeperAuth, out string token)
{
token = string.Empty;
try
{
var storekeeper = _storekeeperBusinessLogicContract.GetStorekeeperByData(storekeeperAuth.Login);
var result = storekeeperAuth.Password == storekeeper.Password;
if (!result)
{
return StorekeeperOperationResponse.Unauthorized("Password are incorrect");
}
token = _jwtProvider.GenerateToken(storekeeper);
return StorekeeperOperationResponse.OK(_mapper.Map<StorekeeperViewModel>(storekeeper));
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception in Login");
return StorekeeperOperationResponse.InternalServerError($"Exception in Login {ex.Message}");
}
}
}

View File

@@ -9,6 +9,7 @@ namespace BankWebApi;
/// </summary>
public class AuthOptions
{
public const string CookieName = "bank";
public const string ISSUER = "Bank_AuthServer"; // издатель токена
public const string AUDIENCE = "Bank_AuthClient"; // потребитель токена
const string KEY = "banksuperpupersecret_secretsecretsecretkey!"; // ключ для шифрации

View File

@@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.4" />

View File

@@ -2,6 +2,7 @@
using BankContracts.BindingModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace BankWebApi.Controllers;
@@ -39,7 +40,8 @@ public class ClerksController(IClerkAdapter adapter) : ControllerBase
/// </summary>
/// <param name="model">модель от пользователя</param>
/// <returns></returns>
[HttpPost]
[HttpPost("register")]
[AllowAnonymous]
public IActionResult Register([FromBody] ClerkBindingModel model)
{
return _adapter.RegisterClerk(model).GetResponse(Request, Response);
@@ -55,4 +57,58 @@ public class ClerksController(IClerkAdapter adapter) : ControllerBase
{
return _adapter.ChangeClerkInfo(model).GetResponse(Request, Response);
}
/// <summary>
/// вход для клерка
/// </summary>
/// <param name="model">модель с логином и паролем</param>
/// <returns></returns>
[HttpPost("login")]
[AllowAnonymous]
public IActionResult Login([FromBody] LoginBindingModel model)
{
var res = _adapter.Login(model, out string token);
if (string.IsNullOrEmpty(token))
{
return res.GetResponse(Request, Response);
}
Response.Cookies.Append(AuthOptions.CookieName, token, new CookieOptions
{
HttpOnly = true,
SameSite = SameSiteMode.None,
Secure = true,
Expires = DateTime.UtcNow.AddDays(2)
});
return res.GetResponse(Request, Response);
}
/// <summary>
/// Получение данных текущего клерка
/// </summary>
/// <returns>Данные кладовщика</returns>
[HttpGet("me")]
public IActionResult GetCurrentUser()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return Unauthorized();
}
var response = _adapter.GetElement(userId);
return response.GetResponse(Request, Response);
}
/// <summary>
/// Выход клерка
/// </summary>
/// <returns></returns>
[HttpPost("logout")]
public IActionResult Logout()
{
Response.Cookies.Delete(AuthOptions.CookieName);
return Ok();
}
}

View File

@@ -1,7 +1,6 @@
using BankContracts.AdapterContracts;
using BankContracts.BindingModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace BankWebApi.Controllers;
@@ -10,7 +9,7 @@ namespace BankWebApi.Controllers;
[Route("api/[controller]/[action]")]
[ApiController]
[Produces("application/json")]
public class PeriodController(IPeriodAdapter adapter) : ControllerBase
public class PeriodsController(IPeriodAdapter adapter) : ControllerBase
{
private readonly IPeriodAdapter _adapter = adapter;

View File

@@ -1,5 +1,6 @@
using BankBusinessLogic.Implementations;
using BankContracts.AdapterContracts;
using BankContracts.BindingModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -8,16 +9,10 @@ namespace BankWebApi.Controllers;
[Authorize]
[Route("api/[controller]/[action]")]
[ApiController]
public class ReportController : ControllerBase
public class ReportController(IReportAdapter adapter) : ControllerBase
{
private readonly IReportAdapter _adapter;
private readonly EmailService _emailService;
public ReportController(IReportAdapter adapter)
{
_adapter = adapter;
_emailService = EmailService.CreateYandexService();
}
private readonly IReportAdapter _adapter = adapter;
private readonly EmailService _emailService = EmailService.CreateYandexService();
[HttpGet]
[Consumes("application/json")]
@@ -84,8 +79,7 @@ public class ReportController : ControllerBase
[Consumes("application/json")]
public async Task<IActionResult> GetDepositAndCreditProgramByCurrency(DateTime fromDate, DateTime toDate, CancellationToken cancellationToken)
{
return (await _adapter.GetDataDepositAndCreditProgramByCurrencyAsync(fromDate, toDate,
cancellationToken)).GetResponse(Request, Response);
return (await _adapter.GetDataDepositAndCreditProgramByCurrencyAsync(fromDate, toDate, cancellationToken)).GetResponse(Request, Response);
}
[HttpGet]
@@ -96,33 +90,28 @@ public class ReportController : ControllerBase
}
[HttpPost]
public async Task<IActionResult> SendReportByCreditProgram(string email, CancellationToken ct)
public async Task<IActionResult> SendReportByCreditProgram([FromBody] MailSendInfoBindingModel model, CancellationToken ct)
{
try
{
var report = await _adapter.CreateDocumentClientsByCreditProgramAsync(ct);
var response = report.GetResponse(Request, Response);
if (response is FileStreamResult fileResult)
var stream = report.GetStream();
var fileName = report.GetFileName() ?? "report.docx";
if (stream == null)
return BadRequest("Не удалось сформировать отчет");
var tempPath = Path.Combine(Path.GetTempPath(), fileName);
using (var fileStream = new FileStream(tempPath, FileMode.Create, FileAccess.Write))
{
var tempPath = Path.GetTempFileName();
using (var fileStream = new FileStream(tempPath, FileMode.Create))
{
await fileResult.FileStream.CopyToAsync(fileStream);
}
await _emailService.SendReportAsync(
toEmail: email,
subject: "Отчет по клиентам по кредитным программам",
body: "<h1>Отчет по клиентам по кредитным программам</h1><p>В приложении находится отчет по клиентам по кредитным программам.</p>",
attachmentPath: tempPath
);
System.IO.File.Delete(tempPath);
return Ok("Отчет успешно отправлен на почту");
await stream.CopyToAsync(fileStream);
}
return BadRequest("Не удалось получить отчет");
await _emailService.SendReportAsync(
toEmail: model.ToEmail,
subject: model.Subject,
body: model.Body,
attachmentPath: tempPath
);
System.IO.File.Delete(tempPath);
return Ok("Отчет успешно отправлен на почту");
}
catch (Exception ex)
{
@@ -131,33 +120,28 @@ public class ReportController : ControllerBase
}
[HttpPost]
public async Task<IActionResult> SendReportByDeposit(string email, DateTime fromDate, DateTime toDate, CancellationToken ct)
public async Task<IActionResult> SendReportByDeposit([FromBody] MailSendInfoBindingModel model, DateTime fromDate, DateTime toDate, CancellationToken ct)
{
try
{
var report = await _adapter.CreateDocumentClientsByDepositAsync(fromDate, toDate, ct);
var response = report.GetResponse(Request, Response);
if (response is FileStreamResult fileResult)
var stream = report.GetStream();
var fileName = report.GetFileName() ?? "report.pdf";
if (stream == null)
return BadRequest("Не удалось сформировать отчет");
var tempPath = Path.Combine(Path.GetTempPath(), fileName);
using (var fileStream = new FileStream(tempPath, FileMode.Create, FileAccess.Write))
{
var tempPath = Path.GetTempFileName();
using (var fileStream = new FileStream(tempPath, FileMode.Create))
{
await fileResult.FileStream.CopyToAsync(fileStream);
}
await _emailService.SendReportAsync(
toEmail: email,
subject: "Отчет по клиентам по вкладам",
body: $"<h1>Отчет по клиентам по вкладам</h1><p>Отчет за период с {fromDate:dd.MM.yyyy} по {toDate:dd.MM.yyyy}</p>",
attachmentPath: tempPath
);
System.IO.File.Delete(tempPath);
return Ok("Отчет успешно отправлен на почту");
await stream.CopyToAsync(fileStream);
}
return BadRequest("Не удалось получить отчет");
await _emailService.SendReportAsync(
toEmail: model.ToEmail,
subject: model.Subject,
body: model.Body,
attachmentPath: tempPath
);
System.IO.File.Delete(tempPath);
return Ok("Отчет успешно отправлен на почту");
}
catch (Exception ex)
{
@@ -166,33 +150,28 @@ public class ReportController : ControllerBase
}
[HttpPost]
public async Task<IActionResult> SendReportByCurrency(string email, DateTime fromDate, DateTime toDate, CancellationToken ct)
public async Task<IActionResult> SendReportByCurrency([FromBody] MailSendInfoBindingModel model, DateTime fromDate, DateTime toDate, CancellationToken ct)
{
try
{
var report = await _adapter.CreateDocumentDepositAndCreditProgramByCurrencyAsync(fromDate, toDate, ct);
var response = report.GetResponse(Request, Response);
if (response is FileStreamResult fileResult)
var stream = report.GetStream();
var fileName = report.GetFileName() ?? "report.pdf";
if (stream == null)
return BadRequest("Не удалось сформировать отчет");
var tempPath = Path.Combine(Path.GetTempPath(), fileName);
using (var fileStream = new FileStream(tempPath, FileMode.Create, FileAccess.Write))
{
var tempPath = Path.GetTempFileName();
using (var fileStream = new FileStream(tempPath, FileMode.Create))
{
await fileResult.FileStream.CopyToAsync(fileStream);
}
await _emailService.SendReportAsync(
toEmail: email,
subject: "Отчет по вкладам и кредитным программам по валютам",
body: $"<h1>Отчет по вкладам и кредитным программам по валютам</h1><p>Отчет за период с {fromDate:dd.MM.yyyy} по {toDate:dd.MM.yyyy}</p>",
attachmentPath: tempPath
);
System.IO.File.Delete(tempPath);
return Ok("Отчет успешно отправлен на почту");
await stream.CopyToAsync(fileStream);
}
return BadRequest("Не удалось получить отчет");
await _emailService.SendReportAsync(
toEmail: model.ToEmail,
subject: model.Subject,
body: model.Body,
attachmentPath: tempPath
);
System.IO.File.Delete(tempPath);
return Ok("Отчет успешно отправлен на почту");
}
catch (Exception ex)
{
@@ -201,33 +180,28 @@ public class ReportController : ControllerBase
}
[HttpPost]
public async Task<IActionResult> SendExcelReportByCreditProgram(string email, CancellationToken ct)
public async Task<IActionResult> SendExcelReportByCreditProgram([FromBody] MailSendInfoBindingModel model, CancellationToken ct)
{
try
{
var report = await _adapter.CreateExcelDocumentClientsByCreditProgramAsync(ct);
var response = report.GetResponse(Request, Response);
if (response is FileStreamResult fileResult)
var stream = report.GetStream();
var fileName = report.GetFileName() ?? "report.xlsx";
if (stream == null)
return BadRequest("Не удалось сформировать отчет");
var tempPath = Path.Combine(Path.GetTempPath(), fileName);
using (var fileStream = new FileStream(tempPath, FileMode.Create, FileAccess.Write))
{
var tempPath = Path.GetTempFileName();
using (var fileStream = new FileStream(tempPath, FileMode.Create))
{
await fileResult.FileStream.CopyToAsync(fileStream);
}
await _emailService.SendReportAsync(
toEmail: email,
subject: "Excel отчет по клиентам по кредитным программам",
body: "<h1>Excel отчет по клиентам по кредитным программам</h1><p>В приложении находится Excel отчет по клиентам по кредитным программам.</p>",
attachmentPath: tempPath
);
System.IO.File.Delete(tempPath);
return Ok("Excel отчет успешно отправлен на почту");
await stream.CopyToAsync(fileStream);
}
return BadRequest("Не удалось получить отчет");
await _emailService.SendReportAsync(
toEmail: model.ToEmail,
subject: model.Subject,
body: model.Body,
attachmentPath: tempPath
);
System.IO.File.Delete(tempPath);
return Ok("Excel отчет успешно отправлен на почту");
}
catch (Exception ex)
{
@@ -236,33 +210,28 @@ public class ReportController : ControllerBase
}
[HttpPost]
public async Task<IActionResult> SendReportDepositByCreditProgram(string email, CancellationToken ct)
public async Task<IActionResult> SendReportDepositByCreditProgram([FromBody] MailSendInfoBindingModel model, CancellationToken ct)
{
try
{
var report = await _adapter.CreateDocumentDepositByCreditProgramAsync(ct);
var response = report.GetResponse(Request, Response);
if (response is FileStreamResult fileResult)
var stream = report.GetStream();
var fileName = report.GetFileName() ?? "report.docx";
if (stream == null)
return BadRequest("Не удалось сформировать отчет");
var tempPath = Path.Combine(Path.GetTempPath(), fileName);
using (var fileStream = new FileStream(tempPath, FileMode.Create, FileAccess.Write))
{
var tempPath = Path.GetTempFileName();
using (var fileStream = new FileStream(tempPath, FileMode.Create))
{
await fileResult.FileStream.CopyToAsync(fileStream);
}
await _emailService.SendReportAsync(
toEmail: email,
subject: "Отчет по вкладам по кредитным программам",
body: "<h1>Отчет по вкладам по кредитным программам</h1><p>В приложении находится отчет по вкладам по кредитным программам.</p>",
attachmentPath: tempPath
);
System.IO.File.Delete(tempPath);
return Ok("Отчет успешно отправлен на почту");
await stream.CopyToAsync(fileStream);
}
return BadRequest("Не удалось получить отчет");
await _emailService.SendReportAsync(
toEmail: model.ToEmail,
subject: model.Subject,
body: model.Body,
attachmentPath: tempPath
);
System.IO.File.Delete(tempPath);
return Ok("Отчет успешно отправлен на почту");
}
catch (Exception ex)
{
@@ -271,33 +240,28 @@ public class ReportController : ControllerBase
}
[HttpPost]
public async Task<IActionResult> SendExcelReportDepositByCreditProgram(string email, CancellationToken ct)
public async Task<IActionResult> SendExcelReportDepositByCreditProgram([FromBody] MailSendInfoBindingModel model, CancellationToken ct)
{
try
{
var report = await _adapter.CreateExcelDocumentDepositByCreditProgramAsync(ct);
var response = report.GetResponse(Request, Response);
if (response is FileStreamResult fileResult)
var stream = report.GetStream();
var fileName = report.GetFileName() ?? "report.xlsx";
if (stream == null)
return BadRequest("Не удалось сформировать отчет");
var tempPath = Path.Combine(Path.GetTempPath(), fileName);
using (var fileStream = new FileStream(tempPath, FileMode.Create, FileAccess.Write))
{
var tempPath = Path.GetTempFileName();
using (var fileStream = new FileStream(tempPath, FileMode.Create))
{
await fileResult.FileStream.CopyToAsync(fileStream);
}
await _emailService.SendReportAsync(
toEmail: email,
subject: "Excel отчет по вкладам по кредитным программам",
body: "<h1>Excel отчет по вкладам по кредитным программам</h1><p>В приложении находится Excel отчет по вкладам по кредитным программам.</p>",
attachmentPath: tempPath
);
System.IO.File.Delete(tempPath);
return Ok("Excel отчет успешно отправлен на почту");
await stream.CopyToAsync(fileStream);
}
return BadRequest("Не удалось получить отчет");
await _emailService.SendReportAsync(
toEmail: model.ToEmail,
subject: model.Subject,
body: model.Body,
attachmentPath: tempPath
);
System.IO.File.Delete(tempPath);
return Ok("Excel отчет успешно отправлен на почту");
}
catch (Exception ex)
{

View File

@@ -2,6 +2,7 @@
using BankContracts.BindingModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace BankWebApi.Controllers;
@@ -39,7 +40,8 @@ public class StorekeepersController(IStorekeeperAdapter adapter) : ControllerBas
/// </summary>
/// <param name="model">модель от пользователя</param>
/// <returns></returns>
[HttpPost]
[HttpPost("register")]
[AllowAnonymous]
public IActionResult Register([FromBody] StorekeeperBindingModel model)
{
return _adapter.RegisterStorekeeper(model).GetResponse(Request, Response);
@@ -55,4 +57,58 @@ public class StorekeepersController(IStorekeeperAdapter adapter) : ControllerBas
{
return _adapter.ChangeStorekeeperInfo(model).GetResponse(Request, Response);
}
/// <summary>
/// вход для кладовщика
/// </summary>
/// <param name="model">модель с логином и паролем</param>
/// <returns></returns>
[HttpPost("login")]
[AllowAnonymous]
public IActionResult Login([FromBody] LoginBindingModel model)
{
var res = _adapter.Login(model, out string token);
if (string.IsNullOrEmpty(token))
{
return res.GetResponse(Request, Response);
}
Response.Cookies.Append(AuthOptions.CookieName, token, new CookieOptions
{
HttpOnly = true,
SameSite = SameSiteMode.None,
Secure = true,
Expires = DateTime.UtcNow.AddDays(2)
});
return res.GetResponse(Request, Response);
}
/// <summary>
/// Получение данных текущего кладовщика
/// </summary>
/// <returns>Данные кладовщика</returns>
[HttpGet("me")]
public IActionResult GetCurrentUser()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return Unauthorized();
}
var response = _adapter.GetElement(userId);
return response.GetResponse(Request, Response);
}
/// <summary>
/// Выход кладовщика
/// </summary>
/// <returns></returns>
[HttpPost("logout")]
public IActionResult Logout()
{
Response.Cookies.Delete(AuthOptions.CookieName);
return Ok();
}
}

View File

@@ -0,0 +1,10 @@
using BankContracts.DataModels;
namespace BankWebApi.Infrastructure;
public interface IJwtProvider
{
string GenerateToken(StorekeeperDataModel dataModel);
string GenerateToken(ClerkDataModel dataModel);
}

View File

@@ -0,0 +1,31 @@
using BankContracts.DataModels;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
namespace BankWebApi.Infrastructure;
public class JwtProvider : IJwtProvider
{
public string GenerateToken(StorekeeperDataModel dataModel)
{
return GenerateToken(dataModel.Id);
}
public string GenerateToken(ClerkDataModel dataModel)
{
return GenerateToken(dataModel.Id);
}
private static string GenerateToken(string id)
{
var token = new JwtSecurityToken(
issuer: AuthOptions.ISSUER,
audience: AuthOptions.AUDIENCE,
claims: [new(ClaimTypes.NameIdentifier, id)],
expires: DateTime.UtcNow.Add(TimeSpan.FromDays(2)),
signingCredentials: new SigningCredentials(AuthOptions.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256));
return new JwtSecurityTokenHandler().WriteToken(token);
}
}

View File

@@ -0,0 +1,10 @@
namespace BankWebApi.Infrastructure;
/// <summary>
/// да пох на это
/// </summary>
public class PasswordHelper
{
public static string HashPassword(string password) => BCrypt.Net.BCrypt.HashPassword(password);
public static bool VerifyPassword(string password, string hash) => BCrypt.Net.BCrypt.Verify(password, hash);
}

View File

@@ -29,12 +29,11 @@ builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Bank API", Version = "v1" });
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> XML-<2D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD>)
// Включение XML-комментариев (если они есть)
var xmlFile = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath, includeControllerXmlComments: true);
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> JWT-<2D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> Swagger UI
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme. Example: 'Bearer {token}'",
@@ -79,13 +78,33 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
IssuerSigningKey = AuthOptions.GetSymmetricSecurityKey(),
ValidateIssuerSigningKey = true,
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
context.Token = context.Request.Cookies[AuthOptions.CookieName];
return Task.CompletedTask;
}
};
});
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowFrontend", policy =>
{
policy.WithOrigins("http://localhost:26312", "http://localhost:3654")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
builder.Services.AddSingleton<IConfigurationDatabase, ConfigurationDatabase>();
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
builder.Services.AddSingleton<IConfigurationDatabase, BankWebApi.Infrastructure.ConfigurationDatabase>();
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
builder.Services.AddTransient<IClerkBusinessLogicContract, ClerkBusinessLogicContract>();
builder.Services.AddTransient<IPeriodBusinessLogicContract, PeriodBusinessLogicContract>();
builder.Services.AddTransient<IDepositBusinessLogicContract, DepositBusinessLogicContract>();
@@ -94,7 +113,7 @@ builder.Services.AddTransient<ICreditProgramBusinessLogicContract, CreditProgram
builder.Services.AddTransient<ICurrencyBusinessLogicContract, CurrencyBusinessLogicContract>();
builder.Services.AddTransient<IStorekeeperBusinessLogicContract, StorekeeperBusinessLogicContract>();
builder.Services.AddTransient<IReplenishmentBusinessLogicContract, ReplenishmentBusinessLogicContract>();
// <20><>
// <20><>
builder.Services.AddTransient<BankDbContext>();
builder.Services.AddTransient<IClerkStorageContract, ClerkStorageContract>();
builder.Services.AddTransient<IPeriodStorageContract, PeriodStorageContract>();
@@ -104,7 +123,7 @@ builder.Services.AddTransient<ICreditProgramStorageContract, CreditProgramStorag
builder.Services.AddTransient<ICurrencyStorageContract, CurrencyStorageContract>();
builder.Services.AddTransient<IStorekeeperStorageContract, StorekeeperStorageContract>();
builder.Services.AddTransient<IReplenishmentStorageContract, ReplenishmentStorageContract>();
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
builder.Services.AddTransient<IClerkAdapter, ClerkAdapter>();
builder.Services.AddTransient<IPeriodAdapter, PeriodAdapter>();
builder.Services.AddTransient<IDepositAdapter, DepositAdapter>();
@@ -118,6 +137,8 @@ builder.Services.AddTransient<IReportAdapter, ReportAdapter>();
builder.Services.AddTransient<BaseWordBuilder, OpenXmlWordBuilder>();
builder.Services.AddTransient<BaseExcelBuilder, OpenXmlExcelBuilder>();
builder.Services.AddTransient<BasePdfBuilder, MigraDocPdfBuilder>();
// shit
builder.Services.AddTransient<IJwtProvider, JwtProvider>();
var app = builder.Build();
@@ -129,7 +150,7 @@ if (app.Environment.IsDevelopment())
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Bank API V1");
c.RoutePrefix = "swagger"; // Swagger UI <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> /swagger
c.RoutePrefix = "swagger"; // Swagger UI <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> /swagger
});
}
if (app.Environment.IsProduction())
@@ -141,11 +162,19 @@ if (app.Environment.IsProduction())
dbContext.Database.Migrate();
}
}
app.UseCors("AllowFrontend");
app.UseHttpsRedirection();
app.UseCookiePolicy(new CookiePolicyOptions
{
HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.Always,
Secure = app.Environment.IsProduction() ? CookieSecurePolicy.Always : CookieSecurePolicy.None
});
app.UseAuthentication();
app.UseAuthorization();
// это для тестов
app.Map("/login/{username}", (string username) =>
{
return new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken(

View File

@@ -13,6 +13,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BankTests", "BankTests\Bank
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BankWebApi", "BankWebApi\BankWebApi.csproj", "{C8A3A3BB-A096-429F-A763-5465C5CB735F}"
EndProject
Project("{54A90642-561A-4BB1-A94E-469ADEE60C69}") = "bankui", "BankUI\bankui.esproj", "{12D5DDAD-F24A-41B0-9FBC-BEFBFB01067D}"
EndProject
Project("{54A90642-561A-4BB1-A94E-469ADEE60C69}") = "bankuiclerk", "BankUIClerk\bankuiclerk.esproj", "{FD37483B-1D73-44B8-9D8B-38FE8F039E48}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -39,6 +43,18 @@ Global
{C8A3A3BB-A096-429F-A763-5465C5CB735F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C8A3A3BB-A096-429F-A763-5465C5CB735F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C8A3A3BB-A096-429F-A763-5465C5CB735F}.Release|Any CPU.Build.0 = Release|Any CPU
{12D5DDAD-F24A-41B0-9FBC-BEFBFB01067D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{12D5DDAD-F24A-41B0-9FBC-BEFBFB01067D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{12D5DDAD-F24A-41B0-9FBC-BEFBFB01067D}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{12D5DDAD-F24A-41B0-9FBC-BEFBFB01067D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{12D5DDAD-F24A-41B0-9FBC-BEFBFB01067D}.Release|Any CPU.Build.0 = Release|Any CPU
{12D5DDAD-F24A-41B0-9FBC-BEFBFB01067D}.Release|Any CPU.Deploy.0 = Release|Any CPU
{FD37483B-1D73-44B8-9D8B-38FE8F039E48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FD37483B-1D73-44B8-9D8B-38FE8F039E48}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FD37483B-1D73-44B8-9D8B-38FE8F039E48}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{FD37483B-1D73-44B8-9D8B-38FE8F039E48}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FD37483B-1D73-44B8-9D8B-38FE8F039E48}.Release|Any CPU.Build.0 = Release|Any CPU
{FD37483B-1D73-44B8-9D8B-38FE8F039E48}.Release|Any CPU.Deploy.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

2
TheBank/bankui/.env Normal file
View File

@@ -0,0 +1,2 @@
VITE_API_URL=https://localhost:7204
# VITE_API_URL=http://localhost:5189

24
TheBank/bankui/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,7 @@
{
"semi": true,
"jsxSingleQuote": false,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "all"
}

View File

@@ -0,0 +1,12 @@
This file explains how Visual Studio created the project.
The following tools were used to generate this project:
- create-vite
The following steps were used to generate this project:
- Create react project with create-vite: `npm init --yes vite@latest bankui -- --template=react-ts`.
- Updating vite.config.ts with port.
- Create project file (`bankui.esproj`).
- Create `launch.json` to enable debugging.
- Add project to solution.
- Write this file.

54
TheBank/bankui/README.md Normal file
View File

@@ -0,0 +1,54 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default tseslint.config({
extends: [
// Remove ...tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
],
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default tseslint.config({
plugins: {
// Add the react-x and react-dom plugins
'react-x': reactX,
'react-dom': reactDom,
},
rules: {
// other rules...
// Enable its recommended typescript rules
...reactX.configs['recommended-typescript'].rules,
...reactDom.configs.recommended.rules,
},
})
```

View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.VisualStudio.JavaScript.Sdk/1.0.1738743">
<PropertyGroup>
<StartupCommand>npm run dev</StartupCommand>
<JavaScriptTestRoot>src\</JavaScriptTestRoot>
<JavaScriptTestFramework>Jest</JavaScriptTestFramework>
<!-- Allows the build (or compile) script located on package.json to run on Build -->
<ShouldRunBuildScript>false</ShouldRunBuildScript>
<!-- Folder where production build objects will be placed -->
<BuildOutputFolder>$(MSBuildProjectDirectory)\dist</BuildOutputFolder>
</PropertyGroup>
</Project>

BIN
TheBank/bankui/bun.lockb Normal file

Binary file not shown.

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

13
TheBank/bankui/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Шрек</title>
</head>
<body>
<div id="root" class="roboto"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,61 @@
{
"name": "bankui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",
"@radix-ui/react-avatar": "^1.1.9",
"@radix-ui/react-checkbox": "^1.3.1",
"@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-menubar": "^1.1.14",
"@radix-ui/react-popover": "^1.1.13",
"@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-separator": "^1.1.6",
"@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-tabs": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.6",
"@tailwindcss/vite": "^4.1.7",
"@tanstack/react-query": "^5.76.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.511.0",
"next-themes": "^0.4.6",
"pdfjs-dist": "^5.2.133",
"react": "^19.1.0",
"react-day-picker": "8.10.1",
"react-dom": "^19.1.0",
"react-hook-form": "^7.56.4",
"react-pdf": "^9.2.1",
"react-router-dom": "^7.6.0",
"sonner": "^2.0.3",
"tailwind-merge": "^3.3.0",
"tailwindcss": "^4.1.7",
"tw-animate-css": "^1.3.0",
"zod": "^3.24.4",
"zustand": "^5.0.4"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@types/node": "^22.15.18",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.5"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px"><path fill="#169154" d="M29,6H15.744C14.781,6,14,6.781,14,7.744v7.259h15V6z"/><path fill="#18482a" d="M14,33.054v7.202C14,41.219,14.781,42,15.743,42H29v-8.946H14z"/><path fill="#0c8045" d="M14 15.003H29V24.005000000000003H14z"/><path fill="#17472a" d="M14 24.005H29V33.055H14z"/><g><path fill="#29c27f" d="M42.256,6H29v9.003h15V7.744C44,6.781,43.219,6,42.256,6z"/><path fill="#27663f" d="M29,33.054V42h13.257C43.219,42,44,41.219,44,40.257v-7.202H29z"/><path fill="#19ac65" d="M29 15.003H44V24.005000000000003H29z"/><path fill="#129652" d="M29 24.005H44V33.055H29z"/></g><path fill="#0c7238" d="M22.319,34H5.681C4.753,34,4,33.247,4,32.319V15.681C4,14.753,4.753,14,5.681,14h16.638 C23.247,14,24,14.753,24,15.681v16.638C24,33.247,23.247,34,22.319,34z"/><path fill="#fff" d="M9.807 19L12.193 19 14.129 22.754 16.175 19 18.404 19 15.333 24 18.474 29 16.123 29 14.013 25.07 11.912 29 9.526 29 12.719 23.982z"/></svg>

After

Width:  |  Height:  |  Size: 998 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="35px" height="35px" viewBox="-4 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.6686 26.0962C25.1812 26.2401 24.4656 26.2563 23.6984 26.145C22.875 26.0256 22.0351 25.7739 21.2096 25.403C22.6817 25.1888 23.8237 25.2548 24.8005 25.6009C25.0319 25.6829 25.412 25.9021 25.6686 26.0962ZM17.4552 24.7459C17.3953 24.7622 17.3363 24.7776 17.2776 24.7939C16.8815 24.9017 16.4961 25.0069 16.1247 25.1005L15.6239 25.2275C14.6165 25.4824 13.5865 25.7428 12.5692 26.0529C12.9558 25.1206 13.315 24.178 13.6667 23.2564C13.9271 22.5742 14.193 21.8773 14.468 21.1894C14.6075 21.4198 14.7531 21.6503 14.9046 21.8814C15.5948 22.9326 16.4624 23.9045 17.4552 24.7459ZM14.8927 14.2326C14.958 15.383 14.7098 16.4897 14.3457 17.5514C13.8972 16.2386 13.6882 14.7889 14.2489 13.6185C14.3927 13.3185 14.5105 13.1581 14.5869 13.0744C14.7049 13.2566 14.8601 13.6642 14.8927 14.2326ZM9.63347 28.8054C9.38148 29.2562 9.12426 29.6782 8.86063 30.0767C8.22442 31.0355 7.18393 32.0621 6.64941 32.0621C6.59681 32.0621 6.53316 32.0536 6.44015 31.9554C6.38028 31.8926 6.37069 31.8476 6.37359 31.7862C6.39161 31.4337 6.85867 30.8059 7.53527 30.2238C8.14939 29.6957 8.84352 29.2262 9.63347 28.8054ZM27.3706 26.1461C27.2889 24.9719 25.3123 24.2186 25.2928 24.2116C24.5287 23.9407 23.6986 23.8091 22.7552 23.8091C21.7453 23.8091 20.6565 23.9552 19.2582 24.2819C18.014 23.3999 16.9392 22.2957 16.1362 21.0733C15.7816 20.5332 15.4628 19.9941 15.1849 19.4675C15.8633 17.8454 16.4742 16.1013 16.3632 14.1479C16.2737 12.5816 15.5674 11.5295 14.6069 11.5295C13.948 11.5295 13.3807 12.0175 12.9194 12.9813C12.0965 14.6987 12.3128 16.8962 13.562 19.5184C13.1121 20.5751 12.6941 21.6706 12.2895 22.7311C11.7861 24.0498 11.2674 25.4103 10.6828 26.7045C9.04334 27.3532 7.69648 28.1399 6.57402 29.1057C5.8387 29.7373 4.95223 30.7028 4.90163 31.7107C4.87693 32.1854 5.03969 32.6207 5.37044 32.9695C5.72183 33.3398 6.16329 33.5348 6.6487 33.5354C8.25189 33.5354 9.79489 31.3327 10.0876 30.8909C10.6767 30.0029 11.2281 29.0124 11.7684 27.8699C13.1292 27.3781 14.5794 27.011 15.985 26.6562L16.4884 26.5283C16.8668 26.4321 17.2601 26.3257 17.6635 26.2153C18.0904 26.0999 18.5296 25.9802 18.976 25.8665C20.4193 26.7844 21.9714 27.3831 23.4851 27.6028C24.7601 27.7883 25.8924 27.6807 26.6589 27.2811C27.3486 26.9219 27.3866 26.3676 27.3706 26.1461ZM30.4755 36.2428C30.4755 38.3932 28.5802 38.5258 28.1978 38.5301H3.74486C1.60224 38.5301 1.47322 36.6218 1.46913 36.2428L1.46884 3.75642C1.46884 1.6039 3.36763 1.4734 3.74457 1.46908H20.263L20.2718 1.4778V7.92396C20.2718 9.21763 21.0539 11.6669 24.0158 11.6669H30.4203L30.4753 11.7218L30.4755 36.2428ZM28.9572 10.1976H24.0169C21.8749 10.1976 21.7453 8.29969 21.7424 7.92417V2.95307L28.9572 10.1976ZM31.9447 36.2428V11.1157L21.7424 0.871022V0.823357H21.6936L20.8742 0H3.74491C2.44954 0 0 0.785336 0 3.75711V36.2435C0 37.5427 0.782956 40 3.74491 40H28.2001C29.4952 39.9997 31.9447 39.2143 31.9447 36.2428Z" fill="#EB5757"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="50px" height="50px"><path d="M 28.8125 0.03125 L 0.8125 5.34375 C 0.339844 5.433594 0 5.863281 0 6.34375 L 0 43.65625 C 0 44.136719 0.339844 44.566406 0.8125 44.65625 L 28.8125 49.96875 C 28.875 49.980469 28.9375 50 29 50 C 29.230469 50 29.445313 49.929688 29.625 49.78125 C 29.855469 49.589844 30 49.296875 30 49 L 30 1 C 30 0.703125 29.855469 0.410156 29.625 0.21875 C 29.394531 0.0273438 29.105469 -0.0234375 28.8125 0.03125 Z M 32 6 L 32 13 L 44 13 L 44 15 L 32 15 L 32 20 L 44 20 L 44 22 L 32 22 L 32 27 L 44 27 L 44 29 L 32 29 L 32 35 L 44 35 L 44 37 L 32 37 L 32 44 L 47 44 C 48.101563 44 49 43.101563 49 42 L 49 8 C 49 6.898438 48.101563 6 47 6 Z M 4.625 15.65625 L 8.1875 15.65625 L 10.21875 28.09375 C 10.308594 28.621094 10.367188 29.355469 10.40625 30.25 L 10.46875 30.25 C 10.496094 29.582031 10.613281 28.855469 10.78125 28.0625 L 13.40625 15.65625 L 16.90625 15.65625 L 19.28125 28.21875 C 19.367188 28.679688 19.433594 29.339844 19.5 30.21875 L 19.53125 30.21875 C 19.558594 29.53125 19.632813 28.828125 19.75 28.125 L 21.75 15.65625 L 25.0625 15.65625 L 21.21875 34.34375 L 17.59375 34.34375 L 15.1875 22.375 C 15.058594 21.75 14.996094 21.023438 14.96875 20.25 L 14.9375 20.25 C 14.875 21.101563 14.769531 21.824219 14.65625 22.375 L 12.1875 34.34375 L 8.4375 34.34375 Z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,51 @@
import { useAuthCheck } from '@/hooks/useAuthCheck';
import { useAuthStore } from '@/store/workerStore';
import { Link, Navigate, Outlet, useLocation } from 'react-router-dom';
import { Header } from '@/components/layout/Header';
import { Footer } from '@/components/layout/Footer';
import { Suspense } from 'react';
import { Button } from './components/ui/button';
function App() {
const user = useAuthStore((store) => store.user);
const { isLoading } = useAuthCheck();
const location = useLocation();
if (isLoading) {
return <div>Loading...</div>;
}
if (!user) {
const redirect = encodeURIComponent(location.pathname + location.search);
return <Navigate to={`/auth?redirect=${redirect}`} replace />;
}
return (
<>
{location.pathname === '/' && (
<main className="flex justify-center items-center">
<div className="flex-1 flex justify-center items-center">
<img className="block" src="/Shrek.png" alt="кладовщик" />
</div>
<div className="flex-1">
<div>Удобный сервис для кладовщиков</div>
<Link to="/storekeepers">
<Button>За работу</Button>
</Link>
</div>
</main>
)}
{location.pathname !== '/' && (
<>
<Header />
<Suspense fallback={<p>Loading...</p>}>
<Outlet />
</Suspense>
</>
)}
<Footer />
</>
);
}
export default App;

View File

@@ -0,0 +1,135 @@
import {
getData,
getSingleData,
postData,
postLoginData,
putData,
} from './client';
import type {
ClientBindingModel,
ClerkBindingModel,
CreditProgramBindingModel,
CurrencyBindingModel,
DepositBindingModel,
PeriodBindingModel,
ReplenishmentBindingModel,
StorekeeperBindingModel,
LoginBindingModel,
} from '../types/types';
// Clients API
export const clientsApi = {
getAll: () => getData<ClientBindingModel>('api/Clients/GetAllRecords'),
getById: (id: string) =>
getData<ClientBindingModel>(`api/Clients/GetRecord/${id}`),
getByClerk: (clerkId: string) =>
getData<ClientBindingModel>(`api/Clients/GetRecordByClerk/${clerkId}`),
create: (data: ClientBindingModel) => postData('api/Clients/Register', data),
update: (data: ClientBindingModel) => putData('api/Clients/ChangeInfo', data),
};
// Clerks API
export const clerksApi = {
getAll: () => getData<ClerkBindingModel>('api/Clerks'),
getById: (id: string) => getData<ClerkBindingModel>(`api/Clerks/${id}`),
create: (data: ClerkBindingModel) => postData('api/Clerks/Register', data),
update: (data: ClerkBindingModel) => putData('api/Clerks/ChangeInfo', data),
};
// Credit Programs API
export const creditProgramsApi = {
getAll: () =>
getData<CreditProgramBindingModel>('api/CreditPrograms/GetAllRecords'),
getById: (id: string) =>
getData<CreditProgramBindingModel>(`api/CreditPrograms/GetRecord/${id}`),
getByStorekeeper: (storekeeperId: string) =>
getData<CreditProgramBindingModel>(
`api/CreditPrograms/GetRecordByStorekeeper/${storekeeperId}`,
),
create: (data: CreditProgramBindingModel) =>
postData('api/CreditPrograms/Register', data),
update: (data: CreditProgramBindingModel) =>
putData('api/CreditPrograms/ChangeInfo', data),
};
// Currencies API
export const currenciesApi = {
getAll: () => getData<CurrencyBindingModel>('api/Currencies/GetAllRecords'),
getById: (id: string) =>
getData<CurrencyBindingModel>(`api/Currencies/GetRecord/${id}`),
getByStorekeeper: (storekeeperId: string) =>
getData<CurrencyBindingModel>(
`api/Currencies/GetRecordByStorekeeper/${storekeeperId}`,
),
create: (data: CurrencyBindingModel) =>
postData('api/Currencies/Register', data),
update: (data: CurrencyBindingModel) =>
putData('api/Currencies/ChangeInfo', data),
};
// Deposits API
export const depositsApi = {
getAll: () => getData<DepositBindingModel>('api/Deposits/GetAllRecords'),
getById: (id: string) =>
getData<DepositBindingModel>(`api/Deposits/GetRecord/${id}`),
getByClerk: (clerkId: string) =>
getData<DepositBindingModel>(`api/Deposits/GetRecordByClerk/${clerkId}`),
create: (data: DepositBindingModel) =>
postData('api/Deposits/Register', data),
update: (data: DepositBindingModel) =>
putData('api/Deposits/ChangeInfo', data),
};
// Periods API
export const periodsApi = {
getAll: () => getData<PeriodBindingModel>('api/Periods/GetAllRecords'),
getById: (id: string) =>
getData<PeriodBindingModel>(`api/Periods/GetRecord/${id}`),
getByStorekeeper: (storekeeperId: string) =>
getData<PeriodBindingModel>(
`api/Period/GetRecordByStorekeeper/${storekeeperId}`,
),
create: (data: PeriodBindingModel) => postData('api/Periods/Register', data),
update: (data: PeriodBindingModel) => putData('api/Periods/ChangeInfo', data),
};
// Replenishments API
export const replenishmentsApi = {
getAll: () =>
getData<ReplenishmentBindingModel>('api/Replenishments/GetAllRecords'),
getById: (id: string) =>
getData<ReplenishmentBindingModel>(`api/Replenishments/GetRecord/${id}`),
getByDeposit: (depositId: string) =>
getData<ReplenishmentBindingModel>(
`api/Replenishments/GetRecordByDeposit/${depositId}`,
),
getByClerk: (clerkId: string) =>
getData<ReplenishmentBindingModel>(
`api/Replenishments/GetRecordByClerk/${clerkId}`,
),
create: (data: ReplenishmentBindingModel) =>
postData('api/Replenishments/Register', data),
update: (data: ReplenishmentBindingModel) =>
putData('api/Replenishments/ChangeInfo', data),
};
// Storekeepers API
export const storekeepersApi = {
getAll: () => getData<StorekeeperBindingModel>('api/storekeepers'),
getById: (id: string) =>
getData<StorekeeperBindingModel>(`api/Storekeepers/GetRecord/${id}`),
create: (data: StorekeeperBindingModel) =>
postData('api/Storekeepers/Register', data),
update: (data: StorekeeperBindingModel) => putData('api/Storekeepers', data),
// auth
login: (data: LoginBindingModel) =>
postLoginData('api/Storekeepers/login', data),
logout: () => postData('api/storekeepers/logout', {}),
getCurrentUser: () =>
getSingleData<StorekeeperBindingModel>('api/storekeepers/me'),
};
//Reports API
export const reportsApi = {
// loadClientsByCreditProgram: () => getReport('path'),
};

View File

@@ -0,0 +1,124 @@
import { ConfigManager } from '@/lib/config';
import type { MailSendInfoBindingModel } from '@/types/types';
const API_URL = ConfigManager.loadUrl();
// Устанавливаем прямой URL к API серверу ASP.NET
// const API_URL = 'https://localhost:7224'; // URL API сервера ASP.NET
export async function getData<T>(path: string): Promise<T[]> {
const res = await fetch(`${API_URL}/${path}`, {
credentials: 'include',
});
if (!res.ok) {
throw new Error(`Не получается загрузить ${path}: ${res.statusText}`);
}
const data = (await res.json()) as T[];
return data;
}
export async function postData<T>(path: string, data: T) {
const res = await fetch(`${API_URL}/${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// mode: 'no-cors',
},
credentials: 'include',
body: JSON.stringify(data),
});
if (!res.ok) {
throw new Error(`Не получается загрузить ${path}: ${res.statusText}`);
}
}
export async function getSingleData<T>(path: string): Promise<T> {
const res = await fetch(`${API_URL}/${path}`, {
credentials: 'include',
});
if (!res.ok) {
throw new Error(`Не получается загрузить ${path}: ${res.statusText}`);
}
const data = (await res.json()) as T;
return data;
}
export async function postLoginData<T>(path: string, data: T): Promise<T> {
const res = await fetch(`${API_URL}/${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(data),
});
if (!res.ok) {
throw new Error(`Не получается загрузить ${path}: ${await res.text()}`);
}
const userData = (await res.json()) as T;
return userData;
}
export async function putData<T>(path: string, data: T) {
const res = await fetch(`${API_URL}/${path}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(data),
});
if (!res.ok) {
throw new Error(`Не получается загрузить ${path}: ${res.statusText}`);
}
}
// report api
export interface ReportParams {
fromDate?: string; // Например, '2025-01-01'
toDate?: string; // Например, '2025-05-21'
}
export type ReportType =
| 'depositByCreditProgram'
| 'depositAndCreditProgramByCurrency';
export type ReportFormat = 'word' | 'excel' | 'pdf';
export async function sendReportByEmail(
reportType: ReportType,
format: ReportFormat,
mailInfo: MailSendInfoBindingModel,
params?: ReportParams,
): Promise<void> {
const actionMap: Record<ReportType, Record<ReportFormat, string>> = {
depositByCreditProgram: {
word: 'SendReportDepositByCreditProgram',
excel: 'SendExcelReportDepositByCreditProgram',
pdf: 'SendReportDepositByCreditProgram',
},
depositAndCreditProgramByCurrency: {
word: 'SendReportByCurrency',
excel: 'SendReportByCurrency',
pdf: 'SendReportByCurrency',
},
};
const action = actionMap[reportType][format];
// Формируем тело запроса
const requestBody = { ...mailInfo, ...params };
const res = await fetch(`${API_URL}/api/Report/${action}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(requestBody),
});
if (!res.ok) {
throw new Error(
`Не удалось отправить отчет ${reportType} (${format}): ${res.statusText}`,
);
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,281 @@
import React, { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Button } from '@/components/ui/button';
import type { CreditProgramBindingModel } from '@/types/types';
import { useAuthStore } from '@/store/workerStore';
import { usePeriods } from '@/hooks/usePeriods';
type BaseFormValues = {
id?: string;
name: string;
cost: number;
maxCost: number;
periodId: string;
};
type EditFormValues = Partial<BaseFormValues>;
const baseSchema = z.object({
id: z.string().optional(),
name: z.string().min(1, 'Название обязательно'),
cost: z.coerce.number().min(0, 'Стоимость не может быть отрицательной'),
maxCost: z.coerce
.number()
.min(0, 'Максимальная стоимость не может быть отрицательной'),
periodId: z.string().min(1, 'Выберите период'),
});
const addSchema = baseSchema;
const editSchema = z.object({
id: z.string().optional(),
name: z.string().min(1, 'Название обязательно').optional(),
cost: z.coerce
.number()
.min(0, 'Стоимость не может быть отрицательной')
.optional(),
maxCost: z.coerce
.number()
.min(0, 'Максимальная стоимость не может быть отрицательной')
.optional(),
periodId: z.string().min(1, 'Выберите период').optional(),
});
interface BaseCreditProgramFormProps {
onSubmit: (data: CreditProgramBindingModel) => void;
schema: z.ZodType<BaseFormValues | EditFormValues>;
defaultValues?: Partial<BaseFormValues>;
}
const BaseCreditProgramForm = ({
onSubmit,
schema,
defaultValues,
}: BaseCreditProgramFormProps): React.JSX.Element => {
const form = useForm<BaseFormValues | EditFormValues>({
resolver: zodResolver(schema),
defaultValues: defaultValues
? {
id: defaultValues.id ?? '',
name: defaultValues.name ?? '',
cost: defaultValues.cost ?? 0,
maxCost: defaultValues.maxCost ?? 0,
periodId: defaultValues.periodId ?? '',
}
: {
id: '',
name: '',
cost: 0,
maxCost: 0,
periodId: '',
},
});
const { periods } = usePeriods();
useEffect(() => {
if (defaultValues) {
form.reset({
id: defaultValues.id ?? '',
name: defaultValues.name ?? '',
cost: defaultValues.cost ?? 0,
maxCost: defaultValues.maxCost ?? 0,
periodId: defaultValues.periodId ?? '',
});
}
}, [defaultValues, form]);
const storekeeper = useAuthStore((store) => store.user);
const handleSubmit = (data: BaseFormValues | EditFormValues) => {
if (!storekeeper?.id) {
console.error('Storekeeper ID is not available.');
return;
}
let payload: CreditProgramBindingModel;
if (schema === addSchema) {
const addData = data as BaseFormValues;
payload = {
id: addData.id || crypto.randomUUID(),
storekeeperId: storekeeper.id,
name: addData.name,
cost: addData.cost,
maxCost: addData.maxCost,
periodId: addData.periodId,
};
} else {
const editData = data as EditFormValues;
const currentDefaultValues = defaultValues as Partial<BaseFormValues>;
const changedData: Partial<CreditProgramBindingModel> = {};
if (editData.id !== undefined && editData.id !== currentDefaultValues?.id)
changedData.id = editData.id;
if (
editData.name !== undefined &&
editData.name !== currentDefaultValues?.name
)
changedData.name = editData.name;
if (
editData.cost !== undefined &&
editData.cost !== currentDefaultValues?.cost
)
changedData.cost = editData.cost;
if (
editData.maxCost !== undefined &&
editData.maxCost !== currentDefaultValues?.maxCost
)
changedData.maxCost = editData.maxCost;
if (
editData.periodId !== undefined &&
editData.periodId !== currentDefaultValues?.periodId
)
changedData.periodId = editData.periodId;
if (currentDefaultValues?.id) changedData.id = currentDefaultValues.id;
changedData.storekeeperId = storekeeper.id;
payload = {
...(defaultValues as CreditProgramBindingModel),
...changedData,
};
}
onSubmit(payload);
};
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 max-w-md mx-auto p-4"
>
<FormField
control={form.control}
name="id"
render={({ field }) => <input type="hidden" {...field} />}
/>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Название</FormLabel>
<FormControl>
<Input placeholder="Название" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="cost"
render={({ field }) => (
<FormItem>
<FormLabel>Стоимость</FormLabel>
<FormControl>
<Input type="number" placeholder="Стоимость" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="maxCost"
render={({ field }) => (
<FormItem>
<FormLabel>Максимальная стоимость</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Максимальная стоимость"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="periodId"
render={({ field }) => (
<FormItem>
<FormLabel>Период</FormLabel>
<Select onValueChange={field.onChange} value={field.value || ''}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Выберите период" />
</SelectTrigger>
</FormControl>
<SelectContent>
{periods &&
periods?.map((period) => (
<SelectItem key={period.id} value={period.id}>
{`${new Date(
period.startTime,
).toLocaleDateString()} - ${new Date(
period.endTime,
).toLocaleDateString()}`}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Сохранить
</Button>
</form>
</Form>
);
};
export const CreditProgramFormAdd = ({
onSubmit,
}: {
onSubmit: (data: CreditProgramBindingModel) => void;
}): React.JSX.Element => {
return <BaseCreditProgramForm onSubmit={onSubmit} schema={addSchema} />;
};
export const CreditProgramFormEdit = ({
onSubmit,
defaultValues,
}: {
onSubmit: (data: CreditProgramBindingModel) => void;
defaultValues: Partial<BaseFormValues>;
}): React.JSX.Element => {
return (
<BaseCreditProgramForm
onSubmit={onSubmit}
schema={editSchema}
defaultValues={defaultValues}
/>
);
};

View File

@@ -0,0 +1,180 @@
import React, { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import type { CurrencyBindingModel } from '@/types/types';
import { useAuthStore } from '@/store/workerStore';
type BaseFormValues = {
id?: string;
name: string;
abbreviation: string;
cost: number;
};
type EditFormValues = {
id?: string;
name?: string;
abbreviation?: string;
cost?: number;
};
const baseSchema = z.object({
id: z.string().optional(),
name: z.string({ message: 'Введите название' }),
abbreviation: z.string({ message: 'Введите аббревиатуру' }),
cost: z.coerce.number({ message: 'Введите стоимость' }),
});
const addSchema = baseSchema;
const editSchema = z.object({
id: z.string().optional(),
name: z.string().min(1, 'Укажите название валюты').optional(),
abbreviation: z.string().min(1, 'Укажите аббревиатуру').optional(),
cost: z.coerce
.number()
.min(0, 'Стоимость не может быть отрицательной')
.optional(),
});
interface BaseCurrencyFormProps {
onSubmit: (data: CurrencyBindingModel) => void;
schema: z.ZodType<BaseFormValues | EditFormValues>;
defaultValues?: Partial<BaseFormValues | EditFormValues>;
}
const BaseCurrencyForm = ({
onSubmit,
schema,
defaultValues,
}: BaseCurrencyFormProps): React.JSX.Element => {
const form = useForm<BaseFormValues | EditFormValues>({
resolver: zodResolver(schema),
defaultValues: {
id: defaultValues?.id || '',
name: defaultValues?.name || '',
abbreviation: defaultValues?.abbreviation || '',
cost: defaultValues?.cost || 0,
},
});
useEffect(() => {
if (defaultValues) {
form.reset({
id: defaultValues.id || '',
name: defaultValues.name || '',
abbreviation: defaultValues.abbreviation || '',
cost: defaultValues.cost || 0,
});
}
}, [defaultValues, form]);
const storekeeper = useAuthStore((store) => store.user);
const handleSubmit = (data: BaseFormValues | EditFormValues) => {
const payload: CurrencyBindingModel = {
id: data.id || crypto.randomUUID(),
storekeeperId: storekeeper?.id,
name: 'name' in data && data.name !== undefined ? data.name : '',
abbreviation:
'abbreviation' in data && data.abbreviation !== undefined
? data.abbreviation
: '',
cost: 'cost' in data && data.cost !== undefined ? data.cost : 0,
};
onSubmit(payload);
};
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 max-w-md mx-auto p-4"
>
<FormField
control={form.control}
name="id"
render={({ field }) => <input type="hidden" {...field} />}
/>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Название</FormLabel>
<FormControl>
<Input placeholder="Например, Доллар США" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="abbreviation"
render={({ field }) => (
<FormItem>
<FormLabel>Аббревиатура</FormLabel>
<FormControl>
<Input placeholder="Например, USD" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="cost"
render={({ field }) => (
<FormItem>
<FormLabel>Стоимость</FormLabel>
<FormControl>
<Input type="number" placeholder="Например, 1.0" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Сохранить
</Button>
</form>
</Form>
);
};
export const CurrencyFormAdd = ({
onSubmit,
}: {
onSubmit: (data: CurrencyBindingModel) => void;
}): React.JSX.Element => {
return <BaseCurrencyForm onSubmit={onSubmit} schema={addSchema} />;
};
export const CurrencyFormEdit = ({
onSubmit,
defaultValues,
}: {
onSubmit: (data: CurrencyBindingModel) => void;
defaultValues: Partial<BaseFormValues>;
}): React.JSX.Element => {
return (
<BaseCurrencyForm
onSubmit={onSubmit}
schema={editSchema}
defaultValues={defaultValues}
/>
);
};

View File

@@ -0,0 +1,82 @@
import type { LoginBindingModel } from '@/types/types';
import { zodResolver } from '@hookform/resolvers/zod';
import React from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '../ui/form';
import { Input } from '../ui/input';
import { Button } from '../ui/button';
interface LoginFormProps {
onSubmit: (data: LoginBindingModel) => void;
defaultValues?: Partial<LoginBindingModel>;
}
const loginFormSchema = z.object({
login: z.string().min(3, 'Логин должен быть не короче 3 символов'),
password: z.string().min(6, 'Пароль должен быть не короче 6 символов'),
});
type FormValues = z.infer<typeof loginFormSchema>;
export const LoginForm = ({
onSubmit,
defaultValues,
}: LoginFormProps): React.JSX.Element => {
const form = useForm<FormValues>({
resolver: zodResolver(loginFormSchema),
defaultValues: {
login: defaultValues?.login || '',
password: defaultValues?.password || '',
},
});
const handleSubmit = (data: FormValues) => {
const payload: LoginBindingModel = {
...data,
};
onSubmit(payload);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
<FormField
control={form.control}
name="login"
render={({ field }) => (
<FormItem>
<FormLabel>Логин</FormLabel>
<FormControl>
<Input placeholder="Логин" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Пароль</FormLabel>
<FormControl>
<Input type="password" placeholder="Пароль" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Войти
</Button>
</form>
</Form>
);
};

View File

@@ -0,0 +1,105 @@
import React from 'react';
import { Document, Page } from 'react-pdf';
import * as pdfjs from 'pdfjs-dist';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import 'react-pdf/dist/esm/Page/TextLayer.css';
import { Button } from '../ui/button';
// Используем встроенный worker
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`;
interface PdfViewerProps {
report: { blob: Blob; fileName: string; mimeType: string } | undefined | null;
}
export const PdfViewer = ({ report }: PdfViewerProps) => {
const [numPages, setNumPages] = React.useState<number | null>(null);
const [pageNumber, setPageNumber] = React.useState(1);
const [pdfUrl, setPdfUrl] = React.useState<string | undefined>(undefined);
const [error, setError] = React.useState<string | null>(null);
// Создаем URL для Blob при изменении report
React.useEffect(() => {
if (report?.blob) {
const url = URL.createObjectURL(report.blob);
setPdfUrl(url);
setError(null);
// Очищаем URL при размонтировании компонента или изменении report
return () => URL.revokeObjectURL(url);
} else {
setPdfUrl(undefined);
}
}, [report]);
const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
setNumPages(numPages);
setPageNumber(1);
};
const handlePrevPage = () => {
setPageNumber((prev) => Math.max(prev - 1, 1));
};
const handleNextPage = () => {
setPageNumber((prev) => Math.min(prev + 1, numPages || 1));
};
if (!pdfUrl) {
return (
<div className="p-4">Загрузка или нет данных для отображения PDF.</div>
);
}
return (
<div className="p-4">
<Document
file={pdfUrl}
onLoadSuccess={onDocumentLoadSuccess}
onLoadError={(error) => {
console.error('Ошибка загрузки PDF:', error);
setError(
'Ошибка при загрузке PDF документа. Пожалуйста, попробуйте снова.',
);
}}
loading={<div className="text-center py-4">Загрузка PDF...</div>}
error={
<div className="text-center text-red-500 py-4">
Не удалось загрузить PDF
</div>
}
>
<Page
pageNumber={pageNumber}
renderTextLayer={false}
renderAnnotationLayer={false}
scale={1.2}
loading={<div className="text-center py-2">Загрузка страницы...</div>}
error={
<div className="text-center text-red-500 py-2">
Ошибка загрузки страницы
</div>
}
/>
</Document>
{error ? (
<div className="text-red-500 py-2">{error}</div>
) : (
<div className="flex justify-between items-center mt-4">
<Button onClick={handlePrevPage} disabled={pageNumber <= 1}>
Предыдущая
</Button>
<p>
Страница {pageNumber} из {numPages || 1}
</p>
<Button
onClick={handleNextPage}
disabled={pageNumber >= (numPages || 1)}
>
Следующая
</Button>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,235 @@
import React, { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Button } from '@/components/ui/button';
import type { PeriodBindingModel } from '@/types/types';
import { Calendar } from '@/components/ui/calendar';
import { format } from 'date-fns';
import { CalendarIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { useAuthStore } from '@/store/workerStore';
type BaseFormValues = {
id?: string;
startTime: Date;
endTime: Date;
};
type EditFormValues = {
id?: string;
startTime?: Date;
endTime?: Date;
};
const baseSchema = z.object({
id: z.string().optional(),
startTime: z.date({
required_error: 'Укажите время начала',
invalid_type_error: 'Неверный формат даты',
}),
endTime: z.date({
required_error: 'Укажите время окончания',
invalid_type_error: 'Неверный формат даты',
}),
});
const addSchema = baseSchema;
const editSchema = z.object({
id: z.string().optional(),
startTime: z
.date({
required_error: 'Укажите время начала',
invalid_type_error: 'Неверный формат даты',
})
.optional(),
endTime: z
.date({
required_error: 'Укажите время окончания',
invalid_type_error: 'Неверный формат даты',
})
.optional(),
});
interface BasePeriodFormProps {
onSubmit: (data: PeriodBindingModel) => void;
schema: z.ZodType<BaseFormValues | EditFormValues>;
defaultValues?: Partial<BaseFormValues | EditFormValues>;
}
const BasePeriodForm = ({
onSubmit,
schema,
defaultValues,
}: BasePeriodFormProps): React.JSX.Element => {
const form = useForm<BaseFormValues | EditFormValues>({
resolver: zodResolver(schema),
defaultValues: {
id: defaultValues?.id || '',
startTime: defaultValues?.startTime || new Date(),
endTime: defaultValues?.endTime || new Date(),
},
});
useEffect(() => {
if (defaultValues) {
form.reset({
id: defaultValues.id || '',
startTime: defaultValues.startTime || new Date(),
endTime: defaultValues.endTime || new Date(),
});
}
}, [defaultValues, form]);
const storekeeper = useAuthStore((store) => store.user);
const handleSubmit = (data: BaseFormValues | EditFormValues) => {
const payload: PeriodBindingModel = {
id: data.id || crypto.randomUUID(),
storekeeperId: storekeeper?.id,
startTime:
'startTime' in data && data.startTime !== undefined
? data.startTime
: new Date(),
endTime:
'endTime' in data && data.endTime !== undefined
? data.endTime
: new Date(),
};
onSubmit(payload);
};
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 max-w-md mx-auto p-4"
>
<FormField
control={form.control}
name="id"
render={({ field }) => <input type="hidden" {...field} />}
/>
<FormField
control={form.control}
name="startTime"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Время начала</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant={'outline'}
className={cn(
'w-full pl-3 text-left font-normal',
!field.value && 'text-muted-foreground',
)}
>
{field.value ? (
format(field.value, 'PPP')
) : (
<span>Выберите дату</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value}
onSelect={field.onChange}
initialFocus
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="endTime"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Время окончания</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant={'outline'}
className={cn(
'w-full pl-3 text-left font-normal',
!field.value && 'text-muted-foreground',
)}
>
{field.value ? (
format(field.value, 'PPP')
) : (
<span>Выберите дату</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value}
onSelect={field.onChange}
initialFocus
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Сохранить
</Button>
</form>
</Form>
);
};
export const PeriodFormAdd = ({
onSubmit,
}: {
onSubmit: (data: PeriodBindingModel) => void;
}): React.JSX.Element => {
return <BasePeriodForm onSubmit={onSubmit} schema={addSchema} />;
};
export const PeriodFormEdit = ({
onSubmit,
defaultValues,
}: {
onSubmit: (data: PeriodBindingModel) => void;
defaultValues: Partial<BaseFormValues>;
}): React.JSX.Element => {
return (
<BasePeriodForm
onSubmit={onSubmit}
schema={editSchema}
defaultValues={defaultValues}
/>
);
};

View File

@@ -0,0 +1,180 @@
import React, { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import type { StorekeeperBindingModel } from '@/types/types';
// Схема для редактирования профиля (все поля опциональны)
const profileEditSchema = z.object({
id: z.string().optional(),
name: z.string().optional(),
surname: z.string().optional(),
middleName: z.string().optional(),
login: z.string().optional(),
email: z.string().email('Неверный формат email').optional(),
phoneNumber: z.string().optional(),
// Пароль, вероятно, должен редактироваться отдельно, но добавим опционально
password: z.string().optional(),
});
type ProfileFormValues = z.infer<typeof profileEditSchema>;
interface ProfileFormProps {
onSubmit: (data: Partial<StorekeeperBindingModel>) => void;
defaultValues: ProfileFormValues;
}
export const ProfileForm = ({
onSubmit,
defaultValues,
}: ProfileFormProps): React.JSX.Element => {
const form = useForm<ProfileFormValues>({
resolver: zodResolver(profileEditSchema),
defaultValues: defaultValues,
});
useEffect(() => {
if (defaultValues) {
form.reset(defaultValues);
}
}, [defaultValues, form]);
const handleSubmit = (data: ProfileFormValues) => {
const changedData: ProfileFormValues = {};
(Object.keys(data) as (keyof ProfileFormValues)[]).forEach((key) => {
changedData[key] = data[key];
if (data[key] !== defaultValues[key]) {
changedData[key] = data[key];
}
});
if (defaultValues.id) {
changedData.id = defaultValues.id;
}
onSubmit(changedData);
};
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 max-w-md mx-auto p-4"
>
<FormField
control={form.control}
name="id"
render={({ field }) => <input type="hidden" {...field} />}
/>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Имя</FormLabel>
<FormControl>
<Input placeholder="Имя" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="surname"
render={({ field }) => (
<FormItem>
<FormLabel>Фамилия</FormLabel>
<FormControl>
<Input placeholder="Фамилия" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="middleName"
render={({ field }) => (
<FormItem>
<FormLabel>Отчество</FormLabel>
<FormControl>
<Input placeholder="Отчество" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="login"
render={({ field }) => (
<FormItem>
<FormLabel>Логин</FormLabel>
<FormControl>
<Input placeholder="Логин" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="Email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phoneNumber"
render={({ field }) => (
<FormItem>
<FormLabel>Номер телефона</FormLabel>
<FormControl>
<Input placeholder="Номер телефона" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Поле пароля можно добавить здесь, если требуется его редактирование */}
{/*
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Новый пароль (оставьте пустым, если не меняете)</FormLabel>
<FormControl>
<Input type="password" placeholder="Пароль" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
*/}
<Button type="submit" className="w-full">
Сохранить изменения
</Button>
</form>
</Form>
);
};

View File

@@ -0,0 +1,166 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import type { StorekeeperBindingModel } from '@/types/types';
interface RegisterFormProps {
onSubmit: (data: StorekeeperBindingModel) => void;
defaultValues?: Partial<StorekeeperBindingModel>;
}
const registerFormSchema = z.object({
id: z.string().optional(),
name: z.string().min(1, 'Имя обязательно'),
surname: z.string().min(1, 'Фамилия обязательна'),
middleName: z.string().min(1, 'Отчество обязательно'),
login: z.string().min(3, 'Логин должен быть не короче 3 символов'),
password: z.string().min(6, 'Пароль должен быть не короче 6 символов'),
email: z.string().email('Введите корректный email'),
phoneNumber: z.string().min(10, 'Введите корректный номер телефона'),
});
type FormValues = z.infer<typeof registerFormSchema>;
export const RegisterForm = ({
onSubmit,
defaultValues,
}: RegisterFormProps): React.JSX.Element => {
const form = useForm<FormValues>({
resolver: zodResolver(registerFormSchema),
defaultValues: {
id: defaultValues?.id || crypto.randomUUID(),
name: defaultValues?.name || '',
surname: defaultValues?.surname || '',
middleName: defaultValues?.middleName || '',
login: defaultValues?.login || '',
password: defaultValues?.password || '',
email: defaultValues?.email || '',
phoneNumber: defaultValues?.phoneNumber || '',
},
});
const handleSubmit = (data: FormValues) => {
const payload: StorekeeperBindingModel = {
...data,
id: data.id || crypto.randomUUID(),
};
onSubmit(payload);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
<FormField
control={form.control}
name="id"
render={({ field }) => <input type="hidden" {...field} />}
/>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Имя</FormLabel>
<FormControl>
<Input placeholder="Имя" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="surname"
render={({ field }) => (
<FormItem>
<FormLabel>Фамилия</FormLabel>
<FormControl>
<Input placeholder="Фамилия" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="middleName"
render={({ field }) => (
<FormItem>
<FormLabel>Отчество</FormLabel>
<FormControl>
<Input placeholder="Отчество" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="login"
render={({ field }) => (
<FormItem>
<FormLabel>Логин</FormLabel>
<FormControl>
<Input placeholder="Логин" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Пароль</FormLabel>
<FormControl>
<Input type="password" placeholder="Пароль" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="Email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phoneNumber"
render={({ field }) => (
<FormItem>
<FormLabel>Номер телефона</FormLabel>
<FormControl>
<Input placeholder="Номер телефона" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Зарегистрировать
</Button>
</form>
</Form>
);
};

View File

@@ -0,0 +1,518 @@
import React from 'react';
import type { SelectedReport } from '../pages/Reports';
import { Button } from '../ui/button';
import { PdfViewer } from './PdfViewer';
import { DialogForm } from '../layout/DialogForm';
import { Input } from '../ui/input';
import { Label } from '../ui/label';
import { toast } from 'sonner';
import { Calendar } from '../ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { cn } from '@/lib/utils';
import { format } from 'date-fns';
import { CalendarIcon } from 'lucide-react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '../ui/form';
import { ConfigManager } from '@/lib/config';
type ReportViewerProps = {
selectedReport: SelectedReport;
};
type ReportData = { blob: Blob; fileName: string; mimeType: string };
const emailFormSchema = z.object({
toEmail: z.string().email({ message: 'Введите корректный email' }),
subject: z.string().min(1, { message: 'Тема обязательна' }),
body: z.string().min(1, { message: 'Текст сообщения обязателен' }),
});
const API_URL = ConfigManager.loadUrl();
export const ReportViewer = ({
selectedReport,
}: ReportViewerProps): React.JSX.Element => {
const [isSendDialogOpen, setIsSendDialogOpen] = React.useState(false);
const [fromDate, setFromDate] = React.useState<Date | undefined>(undefined);
const [toDate, setToDate] = React.useState<Date | undefined>(undefined);
const [isLoading, setIsLoading] = React.useState(false);
const [error, setError] = React.useState<Error | null>(null);
const [report, setReport] = React.useState<ReportData | null>(null);
const form = useForm<z.infer<typeof emailFormSchema>>({
resolver: zodResolver(emailFormSchema),
defaultValues: {
toEmail: '',
subject: getDefaultSubject(selectedReport),
body: getDefaultBody(selectedReport, fromDate, toDate),
},
});
React.useEffect(() => {
form.setValue('subject', getDefaultSubject(selectedReport));
form.setValue('body', getDefaultBody(selectedReport, fromDate, toDate));
setReport(null);
}, [selectedReport, fromDate, toDate, form]);
const getReportTitle = (report: SelectedReport) => {
switch (report) {
case 'word':
return 'Отчет Word по вкладам по кредитным программам';
case 'excel':
return 'Отчет Excel по вкладам по кредитным программам';
case 'pdf':
return 'Отчет PDF по вкладам и кредитным программам по валютам';
default:
return 'Выберите тип отчета';
}
};
function getDefaultSubject(report: SelectedReport | undefined): string {
switch (report) {
case 'word':
return 'Отчет по вкладам по кредитным программам';
case 'excel':
return 'Excel отчет по вкладам по кредитным программам';
case 'pdf':
return 'Отчет по вкладам и кредитным программам по валютам';
default:
return 'Отчет';
}
}
function getDefaultBody(
report: SelectedReport | undefined,
fromDate?: Date,
toDate?: Date,
): string {
switch (report) {
case 'word':
return 'В приложении находится отчет по вкладам по кредитным программам.';
case 'excel':
return 'В приложении находится Excel отчет по вкладам по кредитным программам.';
case 'pdf':
return `В приложении находится отчет по вкладам и кредитным программам по валютам${
fromDate && toDate
? ` за период с ${format(fromDate, 'dd.MM.yyyy')} по ${format(
toDate,
'dd.MM.yyyy',
)}`
: ''
}.`;
default:
return '';
}
}
const getReportUrl = (
selectedReport: SelectedReport,
fromDate?: Date,
toDate?: Date,
): string => {
switch (selectedReport) {
case 'word':
return `${API_URL}/api/Report/LoadDepositByCreditProgram`;
case 'excel':
return `${API_URL}/api/Report/LoadExcelDepositByCreditProgram`;
case 'pdf': {
if (!fromDate || !toDate) {
throw new Error('Необходимо выбрать даты для PDF отчета');
}
const fromDateStr = format(fromDate, 'yyyy-MM-dd');
const toDateStr = format(toDate, 'yyyy-MM-dd');
return `${API_URL}/api/Report/LoadDepositAndCreditProgramByCurrency?fromDate=${fromDateStr}&toDate=${toDateStr}`;
}
default:
throw new Error('Выберите тип отчета');
}
};
const getSendEmailUrl = (selectedReport: SelectedReport): string => {
switch (selectedReport) {
case 'word':
return `${API_URL}/api/Report/SendReportDepositByCreditProgram`;
case 'excel':
return `${API_URL}/api/Report/SendExcelReportDepositByCreditProgram`;
case 'pdf': {
if (!fromDate || !toDate) {
throw new Error('Необходимо выбрать даты для PDF отчета');
}
const fromDateStr = format(fromDate, 'yyyy-MM-dd');
const toDateStr = format(toDate, 'yyyy-MM-dd');
return `${API_URL}/api/Report/SendReportByCurrency?fromDate=${fromDateStr}&toDate=${toDateStr}`;
}
default:
throw new Error('Выберите тип отчета');
}
};
const fetchReport = async (): Promise<ReportData> => {
try {
setIsLoading(true);
setError(null);
const url = getReportUrl(selectedReport, fromDate, toDate);
console.log(`Загружаем отчет с URL: ${url}`);
const acceptHeader =
selectedReport === 'pdf'
? 'application/pdf'
: selectedReport === 'word'
? 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
const response = await fetch(url, {
method: 'GET',
headers: {
Accept: acceptHeader,
},
credentials: 'include',
});
if (!response.ok) {
throw new Error(
`Ошибка загрузки отчета: ${response.status} ${response.statusText}`,
);
}
const blob = await response.blob();
console.log(
`Отчет загружен. Тип: ${blob.type}, размер: ${blob.size} байт`,
);
const contentDisposition = response.headers.get('Content-Disposition');
const defaultExtension =
selectedReport === 'pdf'
? '.pdf'
: selectedReport === 'word'
? '.docx'
: '.xlsx';
let fileName = `report${defaultExtension}`;
if (contentDisposition && contentDisposition.includes('filename=')) {
fileName = contentDisposition
.split('filename=')[1]
.replace(/"/g, '')
.trim();
}
const mimeType = response.headers.get('Content-Type') || '';
const reportData = { blob, fileName, mimeType };
setReport(reportData);
return reportData;
} catch (error) {
console.error('Ошибка при загрузке отчета:', error);
const err =
error instanceof Error ? error : new Error('Неизвестная ошибка');
setError(err);
throw err;
} finally {
setIsLoading(false);
}
};
const handleGenerate = async () => {
try {
await fetchReport();
toast.success('Отчет успешно загружен');
} catch (error) {
toast.error(
`Ошибка загрузки отчета: ${
error instanceof Error ? error.message : 'Неизвестная ошибка'
}`,
);
}
};
const handleDownload = async () => {
try {
let reportData = report;
// Для PDF всегда делаем новый запрос с актуальными датами
if (selectedReport === 'pdf') {
if (!fromDate || !toDate) {
toast.error('Пожалуйста, выберите даты для PDF отчета');
return;
}
toast.loading('Загрузка отчета...');
reportData = await fetchReport();
} else if (!reportData) {
toast.loading('Загрузка отчета...');
reportData = await fetchReport();
}
// Скачиваем отчет
const url = URL.createObjectURL(reportData.blob);
const a = document.createElement('a');
a.href = url;
a.download = reportData.fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success('Отчет успешно скачан');
} catch (error) {
toast.error(
`Ошибка при скачивании отчета: ${
error instanceof Error ? error.message : 'Неизвестная ошибка'
}`,
);
}
};
const handleSendFormSubmit = async (
values: z.infer<typeof emailFormSchema>,
) => {
try {
// Если выбран PDF отчет, проверяем наличие дат
if (selectedReport === 'pdf' && (!fromDate || !toDate)) {
toast.error('Пожалуйста, выберите даты для PDF отчета');
return;
}
setIsLoading(true);
// Формируем данные для отправки
const url = getSendEmailUrl(selectedReport);
// Параметры для запроса
const data: Record<string, string> = {
toEmail: values.toEmail,
subject: values.subject,
body: values.body,
};
// Отправляем запрос
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(data),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Ошибка при отправке отчета: ${response.status} ${response.statusText}\n${errorText}`,
);
}
toast.success('Отчет успешно отправлен на почту');
setIsSendDialogOpen(false);
form.reset();
} catch (error) {
console.error('Ошибка при отправке отчета:', error);
toast.error(
`Ошибка при отправке отчета: ${
error instanceof Error ? error.message : 'Неизвестная ошибка'
}`,
);
} finally {
setIsLoading(false);
}
};
// Проверка, можно ли сгенерировать/скачать/отправить PDF отчет
const isPdfActionDisabled =
selectedReport === 'pdf' && (!fromDate || !toDate || isLoading);
// Отображение ошибки, если она есть
const renderError = () => {
if (!error) return null;
return (
<div className="p-4 border border-red-300 bg-red-50 rounded-md mt-2">
<h3 className="text-red-700 font-semibold mb-1">Детали ошибки:</h3>
<p className="text-red-600 whitespace-pre-wrap break-words">
{error.message}
</p>
</div>
);
};
return (
<div className="w-full">
<div className="text-lg font-semibold mb-4">
{getReportTitle(selectedReport)}
</div>
{/* Кнопки действий */}
<div className="flex gap-4 mb-4">
{/* Кнопка "Сгенерировать" только для PDF с выбранными датами */}
{selectedReport === 'pdf' && (
<Button onClick={handleGenerate} disabled={isPdfActionDisabled}>
{isLoading ? 'Загрузка...' : 'Сгенерировать'}
</Button>
)}
{/* Кнопки "Скачать" и "Отправить" только когда выбран тип отчета */}
{selectedReport && (
<>
<Button
onClick={handleDownload}
disabled={isPdfActionDisabled || isLoading}
>
{isLoading ? 'Загрузка...' : 'Скачать'}
</Button>
<Button
onClick={() => setIsSendDialogOpen(true)}
disabled={isPdfActionDisabled || isLoading}
>
Отправить
</Button>
</>
)}
</div>
{/* Календари для выбора периода для PDF отчета */}
{selectedReport === 'pdf' && (
<div className="flex gap-4 mb-4">
<div className="grid gap-2">
<Label htmlFor="fromDate">От даты</Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant={'outline'}
className={cn(
'w-[240px] justify-start text-left font-normal',
!fromDate && 'text-muted-foreground',
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{fromDate ? (
format(fromDate, 'PPP')
) : (
<span>Выберите дату</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={fromDate}
onSelect={setFromDate}
initialFocus
/>
</PopoverContent>
</Popover>
</div>
<div className="grid gap-2">
<Label htmlFor="toDate">До даты</Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant={'outline'}
className={cn(
'w-[240px] justify-start text-left font-normal',
!toDate && 'text-muted-foreground',
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{toDate ? format(toDate, 'PPP') : <span>Выберите дату</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={toDate}
onSelect={setToDate}
initialFocus
/>
</PopoverContent>
</Popover>
</div>
</div>
)}
{/* Форма отправки отчета на почту */}
<DialogForm
title="Отправка отчета"
description="Введите данные для отправки отчета"
isOpen={isSendDialogOpen}
onClose={() => setIsSendDialogOpen(false)}
onSubmit={form.handleSubmit(handleSendFormSubmit)}
>
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleSendFormSubmit)}
className="space-y-4"
>
<FormField
control={form.control}
name="toEmail"
render={({ field }) => (
<FormItem>
<FormLabel>Email получателя</FormLabel>
<FormControl>
<Input placeholder="example@mail.ru" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="subject"
render={({ field }) => (
<FormItem>
<FormLabel>Тема письма</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="body"
render={({ field }) => (
<FormItem>
<FormLabel>Текст сообщения</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Отправить</Button>
</form>
</Form>
</DialogForm>
<div className="mt-4">
{isLoading && <div className="p-4">Загрузка документа...</div>}
{renderError()}
{!selectedReport && !isLoading && !error && (
<div className="p-4">Выберите тип отчета из боковой панели</div>
)}
{selectedReport && !report && !isLoading && !error && (
<div className="p-4">
{selectedReport === 'pdf'
? 'Выберите даты и нажмите "Сгенерировать"'
: 'Нажмите "Скачать" для загрузки отчета'}
</div>
)}
{selectedReport === 'pdf' && report && <PdfViewer report={report} />}
</div>
</div>
);
};

View File

@@ -0,0 +1,86 @@
import React from 'react';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '../ui/table';
import { Checkbox } from '../ui/checkbox';
type DataTableProps<T> = {
data: T[];
columns: ColumnDef<T>[];
selectedRow?: string;
onRowSelected: (id: string | undefined) => void;
};
export type ColumnDef<T> = {
accessorKey: keyof T | string;
header: string;
renderCell?: (item: T) => React.ReactNode;
};
export const DataTable = <T extends {}>({
data,
columns,
selectedRow,
onRowSelected,
}: DataTableProps<T>): React.JSX.Element => {
const handleRowSelect = (id: string) => {
onRowSelected(selectedRow === id ? undefined : id);
};
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[50px]"></TableHead>
{columns.map((column) => (
<TableHead key={column.accessorKey as string}>
{column.header}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{data.length === 0 ? (
<TableRow>
<TableCell
colSpan={columns.length + 1}
className="h-24 text-center"
>
Нет данных
</TableCell>
</TableRow>
) : (
data.map((item, index) => (
<TableRow
key={(item as any).id || index}
data-state={
selectedRow === (item as any).id ? 'selected' : undefined
}
>
<TableCell>
<Checkbox
checked={selectedRow === (item as any).id}
onCheckedChange={() => handleRowSelect((item as any).id)}
aria-label="Select row"
/>
</TableCell>
{columns.map((column) => (
<TableCell key={column.accessorKey as string}>
{column.renderCell
? column.renderCell(item)
: (item as any)[column.accessorKey] ?? 'N/A'}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
);
};

View File

@@ -0,0 +1,44 @@
import React from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '../ui/dialog';
type DialogFormProps<T> = {
children: React.ReactElement<{ onSubmit: (data: T) => void }>;
title: string;
description: string;
isOpen: boolean;
onClose: () => void;
onSubmit: (data: T) => void;
};
export const DialogForm = <T,>({
title,
description,
children,
isOpen,
onClose,
onSubmit,
}: DialogFormProps<T>): React.JSX.Element => {
console.log(onSubmit);
const wrappedSubmit = (data: T) => {
onSubmit(data);
onClose();
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
{React.cloneElement(children, { onSubmit: wrappedSubmit })}
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,9 @@
import React from 'react';
export const Footer = (): React.JSX.Element => {
return (
<footer className="w-full flex border-t border-black shadow-lg p-2">
<div className="text-black">Банк "Вы банкрот" 2025</div>
</footer>
);
};

View File

@@ -0,0 +1,183 @@
import React from 'react';
import { Link } from 'react-router-dom';
import {
Menubar,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarTrigger,
} from '../ui/menubar';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '../ui/dropdown-menu';
import { Avatar, AvatarFallback } from '../ui/avatar';
import { Button } from '../ui/button';
import { useAuthStore } from '@/store/workerStore';
type NavOptionValue = {
name: string;
link: string;
id: number;
};
type NavOption = {
name: string;
options: NavOptionValue[];
};
const navOptions = [
{
name: 'Валюты',
options: [
{
id: 1,
name: 'Просмотреть',
link: '/currencies',
},
],
},
{
name: 'Кредитные программы',
options: [
{
id: 1,
name: 'Просмотреть',
link: '/credit-programs',
},
],
},
{
name: 'Сроки',
options: [
{
id: 1,
name: 'Просмотреть',
link: '/periods',
},
],
},
{
name: 'Кладовщики',
options: [
{
id: 1,
name: 'Просмотреть',
link: '/storekeepers',
},
],
},
{
name: 'Вклады',
options: [
{
id: 1,
name: 'Управление валютами вкладов',
link: '/deposit-currencies',
},
],
},
{
name: 'Отчеты',
options: [
{
id: 1,
name: 'Выгрузить отчеты',
link: '/reports',
},
],
},
];
export const Header = (): React.JSX.Element => {
const user = useAuthStore((store) => store.user);
const logout = useAuthStore((store) => store.logout);
const { logout: serverLogout } = useAuthStore();
const loggedOut = () => {
serverLogout();
logout();
};
const fullName = `${user?.name ?? ''} ${user?.surname ?? ''}`;
return (
<header className="flex w-full p-2 justify-between">
<nav className="text-black">
<Menubar className="flex gap-10">
{navOptions.map((item) => (
<MenuOption item={item} key={item.name} />
))}
</Menubar>
</nav>
<div>
<ProfileIcon name={fullName || 'Евгений Эгов'} logout={loggedOut} />
</div>
</header>
);
};
const MenuOption = ({ item }: { item: NavOption }) => {
return (
<MenubarMenu>
<MenubarTrigger className="">{item.name}</MenubarTrigger>
<MenubarContent className="">
{item.options.map((x, i) => (
<React.Fragment key={x.id}>
{i == 1 && item.options.length > 1 && <MenubarSeparator />}
<MenubarItem className="">
<Link className="" to={x.link}>
{x.name}
</Link>
</MenubarItem>
</React.Fragment>
))}
<MenubarSeparator />
</MenubarContent>
</MenubarMenu>
);
};
type ProfileIconProps = {
name: string;
logout: () => void;
};
export const ProfileIcon = ({
name,
logout,
}: ProfileIconProps): React.JSX.Element => {
return (
<div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Avatar className="h-9 w-9">
<AvatarFallback>{name[0]}</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem className="font-bold text-lg">
{name}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link to="/profile" className="block w-full text-left">
Профиль
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Button
onClick={logout}
variant="outline"
className="block w-full text-left"
>
Выйти
</Button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};

View File

@@ -0,0 +1,58 @@
import React from 'react';
import {
Sidebar,
SidebarContent,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarTrigger,
} from '@/components/ui/sidebar';
type SidebarProps = {
onWordClick: () => void;
onPdfClick: () => void;
onExcelClick: () => void;
};
export const ReportSidebar = ({
onWordClick,
onExcelClick,
onPdfClick,
}: SidebarProps): React.JSX.Element => {
return (
<SidebarProvider className="w-[400px]">
<Sidebar variant="floating" collapsible="none">
<SidebarContent />
<SidebarGroupContent className="">
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild onClick={onWordClick}>
<span>
<img src="/icons/word.svg" alt="word-icon" />
отчет word КЛАДОВЩИКА
</span>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton asChild onClick={onExcelClick}>
<span>
<img src="/icons/excel.svg" alt="excel-icon" />
отчет excel КЛАДОВЩИКА
</span>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton asChild onClick={onPdfClick}>
<span className="p-5">
<img src="/icons/pdf.svg" alt="pdf-icon" />
отчет pdf КЛАДОВЩИКА
</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</Sidebar>
</SidebarProvider>
);
};

View File

@@ -0,0 +1,64 @@
import React from 'react';
import {
Sidebar,
SidebarContent,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarTrigger,
} from '@/components/ui/sidebar';
import { Plus, Pencil } from 'lucide-react';
import { Link } from 'react-router-dom';
type SidebarProps = {
onAddClick: () => void;
onEditClick: () => void;
};
const availableTasks = [
{
name: 'Добавить',
link: '',
},
{
name: 'Редактировать',
link: '',
},
];
export const AppSidebar = ({
onAddClick,
onEditClick,
}: SidebarProps): React.JSX.Element => {
return (
<SidebarProvider>
<Sidebar variant="floating" collapsible="icon">
<SidebarTrigger />
<SidebarContent />
<SidebarGroupContent className="">
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild onClick={onAddClick}>
<Link to={availableTasks[0].link}>
<Plus />
<span>{availableTasks[0].name}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton asChild onClick={onEditClick}>
<Link to={availableTasks[1].link}>
<Pencil />
<span>{availableTasks[1].name}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</Sidebar>
</SidebarProvider>
);
};

View File

@@ -0,0 +1,76 @@
import { useStorekeepers } from '@/hooks/useStorekeepers';
import React from 'react';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../ui/tabs';
import { RegisterForm } from '../features/RegisterForm';
import { LoginForm } from '../features/LoginForm';
import { toast } from 'sonner';
import type { LoginBindingModel, StorekeeperBindingModel } from '@/types/types';
type Forms = 'login' | 'register';
export const AuthStorekeeper = (): React.JSX.Element => {
const {
createStorekeeper,
loginStorekeeper,
isLoginError,
loginError,
isCreateError,
} = useStorekeepers();
const [currentForm, setCurrentForm] = React.useState<Forms>('login');
const handleRegister = (data: StorekeeperBindingModel) => {
createStorekeeper(data, {
onSuccess: () => {
toast('Регистрация успешна! Войдите в систему.');
},
onError: (error) => {
toast(`Ошибка регистрации: ${error.message}`);
},
});
};
const handleLogin = (data: LoginBindingModel) => {
loginStorekeeper(data);
};
React.useEffect(() => {
if (isLoginError) {
toast(`Ошибка входа: ${loginError?.message}`);
}
if (isCreateError) {
toast('Ошибка при регистрации');
}
}, [isLoginError, loginError, isCreateError]);
return (
<>
<main className="flex flex-col justify-center items-center">
<div>
<Tabs defaultValue="login" className="w-[400px]">
<TabsList>
<TabsTrigger
onClick={() => setCurrentForm('login')}
value="login"
>
Вход
</TabsTrigger>
<TabsTrigger
onClick={() => setCurrentForm('register')}
value="register"
>
Регистрация
</TabsTrigger>
</TabsList>
<TabsContent value={currentForm}>
<LoginForm onSubmit={handleLogin} />
</TabsContent>
<TabsContent value="register">
<RegisterForm onSubmit={handleRegister} />
</TabsContent>
</Tabs>
</div>
</main>
</>
);
};

View File

@@ -0,0 +1,183 @@
import React from 'react';
import { AppSidebar } from '../layout/Sidebar';
import { useCreditPrograms } from '@/hooks/useCreditPrograms';
import { DialogForm } from '../layout/DialogForm';
import { DataTable } from '../layout/DataTable';
import {
CreditProgramFormAdd,
CreditProgramFormEdit,
} from '../features/CreditProgramForm';
import type { CreditProgramBindingModel } from '@/types/types';
import type { ColumnDef } from '../layout/DataTable';
import { toast } from 'sonner';
import { usePeriods } from '@/hooks/usePeriods';
import { useStorekeepers } from '@/hooks/useStorekeepers';
interface CreditProgramTableData extends CreditProgramBindingModel {
formattedPeriod: string;
storekeeperFullName: string;
}
const columns: ColumnDef<CreditProgramTableData>[] = [
{
accessorKey: 'id',
header: 'ID',
},
{
accessorKey: 'name',
header: 'Название',
},
{
accessorKey: 'cost',
header: 'Стоимость',
},
{
accessorKey: 'maxCost',
header: 'Макс. стоимость',
},
{
accessorKey: 'storekeeperFullName',
header: 'Кладовщик',
},
{
accessorKey: 'formattedPeriod',
header: 'Период',
},
];
export const CreditPrograms = (): React.JSX.Element => {
const {
isLoading,
isError,
error,
creditPrograms,
createCreditProgram,
updateCreditProgram,
} = useCreditPrograms();
const { periods } = usePeriods();
const { storekeepers } = useStorekeepers();
const finalData = React.useMemo(() => {
if (!creditPrograms || !periods || !storekeepers) return [];
return creditPrograms.map((program) => {
const period = periods?.find((p) => p.id === program.periodId);
const storekeeper = storekeepers?.find(
(s) => s.id === program.storekeeperId,
);
const formattedPeriod = period
? `${new Date(period.startTime).toLocaleDateString()} - ${new Date(
period.endTime,
).toLocaleDateString()}`
: 'Неизвестный период';
const storekeeperFullName = storekeeper
? [storekeeper.surname, storekeeper.name, storekeeper.middleName]
.filter(Boolean)
.join(' ') || 'Неизвестный кладовщик'
: 'Неизвестный кладовщик';
return {
...program,
formattedPeriod,
storekeeperFullName,
};
});
}, [creditPrograms, periods, storekeepers]);
const [isAddDialogOpen, setIsAddDialogOpen] = React.useState<boolean>(false);
const [isEditDialogOpen, setIsEditDialogOpen] =
React.useState<boolean>(false);
const [selectedItem, setSelectedItem] = React.useState<
CreditProgramBindingModel | undefined
>();
const handleAdd = (data: CreditProgramBindingModel) => {
console.log('add', data);
createCreditProgram(data);
setIsAddDialogOpen(false);
};
const handleEdit = (data: CreditProgramBindingModel) => {
if (selectedItem) {
updateCreditProgram({
...selectedItem,
...data,
});
console.log('edit', data);
setIsEditDialogOpen(false);
setSelectedItem(undefined);
}
};
const handleSelectItem = (id: string | undefined) => {
const item = creditPrograms?.find((cp) => cp.id === id);
setSelectedItem(item);
};
const openEditForm = () => {
if (!selectedItem) {
toast.error('Выберите элемент для редактирования');
return;
}
setIsEditDialogOpen(true);
};
if (isLoading) {
return <main className="container mx-auto py-10">Загрузка...</main>;
}
if (isError) {
return (
<main className="container mx-auto py-10">
Ошибка загрузки: {error?.message}
</main>
);
}
return (
<main className="flex-1 flex relative">
<AppSidebar
onAddClick={() => {
setIsAddDialogOpen(true);
}}
onEditClick={() => {
openEditForm();
}}
/>
<div className="flex-1 p-4">
<DialogForm<CreditProgramBindingModel>
title="Форма кредитной программы"
description="Добавить новую кредитную программу"
isOpen={isAddDialogOpen}
onClose={() => setIsAddDialogOpen(false)}
onSubmit={handleAdd}
>
<CreditProgramFormAdd />
</DialogForm>
{selectedItem && (
<DialogForm<CreditProgramBindingModel>
title="Форма кредитной программы"
description="Изменить кредитную программу"
isOpen={isEditDialogOpen}
onClose={() => setIsEditDialogOpen(false)}
onSubmit={handleEdit}
>
<CreditProgramFormEdit defaultValues={selectedItem} />
</DialogForm>
)}
<div className="">
<DataTable
data={finalData}
columns={columns}
onRowSelected={(id) => handleSelectItem(id)}
selectedRow={selectedItem?.id}
/>
</div>
</div>
</main>
);
};

View File

@@ -0,0 +1,163 @@
import React from 'react';
import { AppSidebar } from '../layout/Sidebar';
import { DialogForm } from '../layout/DialogForm';
import { DataTable } from '../layout/DataTable';
import { useCurrencies } from '@/hooks/useCurrencies';
import { useStorekeepers } from '@/hooks/useStorekeepers';
import type { CurrencyBindingModel } from '@/types/types';
import type { ColumnDef } from '../layout/DataTable';
import { CurrencyFormAdd, CurrencyFormEdit } from '../features/CurrencyForm';
import { toast } from 'sonner';
interface CurrencyTableData extends CurrencyBindingModel {
storekeeperName: string;
}
const columns: ColumnDef<CurrencyTableData>[] = [
{
accessorKey: 'id',
header: 'ID',
},
{
accessorKey: 'name',
header: 'Название',
},
{
accessorKey: 'abbreviation',
header: 'Аббревиатура',
},
{
accessorKey: 'cost',
header: 'Стоимость',
},
{
accessorKey: 'storekeeperName',
header: 'Кладовщик',
},
];
export const Currencies = (): React.JSX.Element => {
const {
isLoading,
isError,
error,
currencies,
createCurrency,
updateCurrency,
} = useCurrencies();
const { storekeepers } = useStorekeepers();
const finalData = React.useMemo(() => {
if (!currencies || !storekeepers) return [];
return currencies.map((currency) => {
const storekeeper = storekeepers.find(
(s) => s.id === currency.storekeeperId,
);
const storekeeperName = storekeeper
? [storekeeper.surname, storekeeper.name, storekeeper.middleName]
.filter(Boolean)
.join(' ') || 'Неизвестный кладовщик'
: 'Неизвестный кладовщик';
return {
...currency,
storekeeperName,
};
});
}, [currencies, storekeepers]);
const [isAddDialogOpen, setIsAddDialogOpen] = React.useState<boolean>(false);
const [isEditDialogOpen, setIsEditDialogOpen] =
React.useState<boolean>(false);
const [selectedItem, setSelectedItem] = React.useState<
CurrencyBindingModel | undefined
>();
const handleAdd = (data: CurrencyBindingModel) => {
console.log('add', data);
createCurrency(data);
setIsAddDialogOpen(false);
};
const handleEdit = (data: CurrencyBindingModel) => {
if (selectedItem) {
updateCurrency({
...selectedItem,
...data,
});
console.log('edit', data);
setIsEditDialogOpen(false);
setSelectedItem(undefined);
}
};
const handleSelectItem = (id: string | undefined) => {
const item = currencies?.find((c) => c.id === id);
setSelectedItem(item);
};
const openEditForm = () => {
if (!selectedItem) {
toast.error('Выберите элемент для редактирования');
return;
}
setIsEditDialogOpen(true);
};
if (isLoading) {
return <main className="container mx-auto py-10">Загрузка...</main>;
}
if (isError) {
return (
<main className="container mx-auto py-10">
Ошибка загрузки: {error?.message}
</main>
);
}
return (
<main className="flex-1 flex relative">
<AppSidebar
onAddClick={() => {
setIsAddDialogOpen(true);
}}
onEditClick={() => {
openEditForm();
}}
/>
<div className="flex-1 p-4">
<DialogForm<CurrencyBindingModel>
title="Форма валюты"
description="Добавьте новую валюту"
isOpen={isAddDialogOpen}
onClose={() => setIsAddDialogOpen(false)}
onSubmit={handleAdd}
>
<CurrencyFormAdd onSubmit={handleAdd} />
</DialogForm>
{selectedItem && (
<DialogForm<CurrencyBindingModel>
title="Форма валюты"
description="Измените валюту"
isOpen={isEditDialogOpen}
onClose={() => setIsEditDialogOpen(false)}
onSubmit={handleEdit}
>
<CurrencyFormEdit defaultValues={selectedItem} />
</DialogForm>
)}
<div>
<DataTable
data={finalData}
columns={columns}
onRowSelected={(id) => handleSelectItem(id)}
selectedRow={selectedItem?.id}
/>
</div>
</div>
</main>
);
};

View File

@@ -0,0 +1,367 @@
import React from 'react';
import { useDeposits } from '@/hooks/useDeposits';
import { useCurrencies } from '@/hooks/useCurrencies';
import { useClerks } from '@/hooks/useClerks';
import { DataTable, type ColumnDef } from '../layout/DataTable';
import { AppSidebar } from '../layout/Sidebar';
import { DialogForm } from '../layout/DialogForm';
import { toast } from 'sonner';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { cn } from '@/lib/utils';
import type {
DepositBindingModel,
DepositCurrencyBindingModel,
} from '@/types/types';
type DepositRowData = DepositBindingModel & {
clerkName: string;
currenciesDisplay: string;
};
const columns: ColumnDef<DepositRowData>[] = [
{
accessorKey: 'id',
header: 'ID',
},
{
accessorKey: 'interestRate',
header: 'Процентная ставка',
},
{
accessorKey: 'cost',
header: 'Стоимость',
},
{
accessorKey: 'period',
header: 'Срок вклада',
},
{
accessorKey: 'clerkName',
header: 'Клерк',
},
{
accessorKey: 'currenciesDisplay',
header: 'Валюты',
},
];
type FormValues = {
currencyIds: string[];
};
const schema = z.object({
currencyIds: z.array(z.string()),
});
const DepositCurrencyForm = ({
onSubmit,
defaultValues,
}: {
onSubmit: (data: { currencyIds: string[] }) => void;
defaultValues: Partial<DepositBindingModel>;
}): React.JSX.Element => {
const { currencies } = useCurrencies();
const initialCurrencyIds = React.useMemo(
() =>
defaultValues?.depositCurrencies
?.map((dc) => dc.currencyId)
.filter((id): id is string => !!id) || [],
[defaultValues?.depositCurrencies],
);
const form = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
currencyIds: initialCurrencyIds,
},
});
React.useEffect(() => {
if (defaultValues) {
form.reset({
currencyIds: initialCurrencyIds,
});
}
}, [defaultValues, form, initialCurrencyIds]);
const handleSubmit = (data: FormValues) => {
onSubmit(data);
};
const selectedCurrencyIds = form.watch('currencyIds') || [];
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 max-w-md mx-auto p-4"
>
<FormField
control={form.control}
name="currencyIds"
render={({ field }) => (
<FormItem>
<FormLabel>Валюты</FormLabel>
<Select
onValueChange={(value) => {
const currentValues = field.value || [];
if (!currentValues.includes(value)) {
field.onChange([...currentValues, value]);
}
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Выберите валюты" />
</SelectTrigger>
</FormControl>
<SelectContent>
{currencies?.map((currency) => (
<SelectItem
key={currency.id}
value={currency.id || ''}
className={cn(
selectedCurrencyIds.includes(currency.id || '') &&
'bg-muted',
)}
>
{`${currency.name} (${currency.abbreviation})`}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="flex flex-wrap gap-2 mt-2">
{selectedCurrencyIds.map((currencyId) => {
const currency = currencies?.find((c) => c.id === currencyId);
return (
<div
key={currencyId}
className="bg-muted px-2 py-1 rounded-md flex items-center gap-1"
>
<span>
{currency
? `${currency.name} (${currency.abbreviation})`
: currencyId}
</span>
<Button
type="button"
variant="ghost"
size="icon"
className="h-4 w-4 rounded-full"
onClick={() => {
const newValues = selectedCurrencyIds.filter(
(id) => id !== currencyId,
);
form.setValue('currencyIds', newValues);
}}
>
×
</Button>
</div>
);
})}
</div>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Сохранить
</Button>
</form>
</Form>
);
};
export const DepositCurrencyManager = (): React.JSX.Element => {
const {
deposits,
isLoading: isDepositsLoading,
error: depositsError,
updateDeposit,
} = useDeposits();
const { currencies, isLoading: isCurrenciesLoading } = useCurrencies();
const { clerks, isLoading: isClerksLoading } = useClerks();
const [isEditDialogOpen, setIsEditDialogOpen] =
React.useState<boolean>(false);
const [selectedItem, setSelectedItem] = React.useState<
DepositBindingModel | undefined
>();
const finalData = React.useMemo(() => {
if (!deposits || !currencies || !clerks) return [];
return deposits.map((deposit) => {
// Находим клерка по ID
const clerk = clerks.find((c) => c.id === deposit.clerkId);
// Формирование списка валют
const currenciesDisplay =
deposit.depositCurrencies
?.map((dc) => {
const currency = currencies?.find((c) => c.id === dc.currencyId);
return currency
? `${currency.name} (${currency.abbreviation})`
: dc.currencyId;
})
.join(', ') || 'Нет валют';
return {
...deposit,
clerkName: clerk
? `${clerk.name} ${clerk.surname}`
: 'Неизвестный клерк',
currenciesDisplay,
};
});
}, [deposits, currencies, clerks]);
const handleEdit = (data: { currencyIds: string[] }) => {
if (selectedItem) {
// Проверка на дублирование валют
const uniqueCurrencyIds = new Set(data.currencyIds);
if (uniqueCurrencyIds.size !== data.currencyIds.length) {
toast.error(
'Обнаружены дублирующиеся валюты. Пожалуйста, убедитесь что каждая валюта выбрана только один раз.',
);
return;
}
// Формируем массив связей, сохраняя существующие ID где это возможно
const depositCurrencies: DepositCurrencyBindingModel[] =
data.currencyIds.map((currencyId) => {
// Ищем существующую связь с этой валютой
const existingRelation = selectedItem.depositCurrencies?.find(
(dc) => dc.currencyId === currencyId,
);
// Если связь уже существует, возвращаем её с оригинальным ID
if (existingRelation) {
return { ...existingRelation };
}
// Если это новая связь, создаем объект без ID
return {
currencyId,
depositId: selectedItem.id,
};
});
console.log('Обновляем депозит с данными:', {
...selectedItem,
depositCurrencies,
});
// Обновляем вклад, сохраняя все оригинальные поля и связи
updateDeposit({
...selectedItem, // Сохраняем все существующие поля
depositCurrencies, // Обновляем только связи с валютами
});
setIsEditDialogOpen(false);
setSelectedItem(undefined);
toast.success('Связи валют с вкладом успешно обновлены');
}
};
const handleSelectItem = (id: string | undefined) => {
const item = deposits?.find((p) => p.id === id);
if (item) {
setSelectedItem({
...item,
});
} else {
setSelectedItem(undefined);
}
};
const openEditForm = () => {
if (!selectedItem) {
toast('Выберите вклад для добавления валют');
return;
}
setIsEditDialogOpen(true);
};
if (isDepositsLoading || isCurrenciesLoading || isClerksLoading) {
return <main className="container mx-auto py-10">Загрузка...</main>;
}
if (depositsError) {
return (
<main className="container mx-auto py-10">
Ошибка загрузки данных: {depositsError.message}
</main>
);
}
return (
<main className="flex-1 flex relative">
<AppSidebar
onAddClick={() => {
toast(
'Кладовщик не может создавать вклады, только связывать их с валютами',
);
}}
onEditClick={() => {
openEditForm();
}}
/>
<div className="flex-1 p-4">
{selectedItem && (
<DialogForm
title="Управление валютами вклада"
description="Выберите валюты для связи с вкладом"
isOpen={isEditDialogOpen}
onClose={() => setIsEditDialogOpen(false)}
onSubmit={handleEdit}
>
<DepositCurrencyForm
onSubmit={handleEdit}
defaultValues={selectedItem}
/>
</DialogForm>
)}
<div className="mb-4">
<h2 className="text-2xl font-bold">
Управление связями вкладов и валют
</h2>
<p className="text-muted-foreground">
Кладовщик может связывать существующие вклады с валютами, но не
может создавать новые вклады.
</p>
</div>
<div>
<DataTable
data={finalData}
columns={columns}
onRowSelected={(id) => handleSelectItem(id)}
selectedRow={selectedItem?.id}
/>
</div>
</div>
</main>
);
};

View File

@@ -0,0 +1,163 @@
import React from 'react';
import { AppSidebar } from '../layout/Sidebar';
import { DialogForm } from '../layout/DialogForm';
import { DataTable } from '../layout/DataTable';
import { usePeriods } from '@/hooks/usePeriods';
import { useStorekeepers } from '@/hooks/useStorekeepers';
import type { PeriodBindingModel } from '@/types/types';
import type { ColumnDef } from '../layout/DataTable';
import { PeriodFormAdd, PeriodFormEdit } from '../features/PeriodForm';
import { toast } from 'sonner';
interface PeriodTableData extends PeriodBindingModel {
storekeeperName: string;
}
const columns: ColumnDef<PeriodTableData>[] = [
{
accessorKey: 'id',
header: 'ID',
},
{
accessorKey: 'startTime',
header: 'Время начала',
renderCell: (item) => new Date(item.startTime).toLocaleDateString(),
},
{
accessorKey: 'endTime',
header: 'Время окончания',
renderCell: (item) => new Date(item.endTime).toLocaleDateString(),
},
{
accessorKey: 'storekeeperName',
header: 'Кладовщик',
},
];
export const Periods = (): React.JSX.Element => {
const { isLoading, isError, error, periods, createPeriod, updatePeriod } =
usePeriods();
const { storekeepers } = useStorekeepers();
const finalData = React.useMemo(() => {
if (!periods || !storekeepers) return [];
return periods.map((period) => {
const storekeeper = storekeepers.find(
(s) => s.id === period.storekeeperId,
);
const storekeeperName = storekeeper
? [storekeeper.surname, storekeeper.name, storekeeper.middleName]
.filter(Boolean)
.join(' ') || 'Неизвестный кладовщик'
: 'Неизвестный кладовщик';
return {
...period,
storekeeperName,
};
});
}, [periods, storekeepers]);
const [isAddDialogOpen, setIsAddDialogOpen] = React.useState<boolean>(false);
const [isEditDialogOpen, setIsEditDialogOpen] =
React.useState<boolean>(false);
const [selectedItem, setSelectedItem] = React.useState<
PeriodBindingModel | undefined
>();
const handleAdd = (data: PeriodBindingModel) => {
createPeriod(data);
setIsAddDialogOpen(false);
};
const handleEdit = (data: PeriodBindingModel) => {
if (selectedItem) {
updatePeriod({
...selectedItem,
...data,
});
setIsEditDialogOpen(false);
setSelectedItem(undefined);
}
};
const handleSelectItem = (id: string | undefined) => {
const item = periods?.find((p) => p.id === id);
if (item) {
setSelectedItem({
...item,
startTime: new Date(item.startTime),
endTime: new Date(item.endTime),
});
} else {
setSelectedItem(undefined);
}
};
const openEditForm = () => {
if (!selectedItem) {
toast.error('Выберите элемент для редактирования');
return;
}
setIsEditDialogOpen(true);
};
if (isLoading) {
return <main className="container mx-auto py-10">Загрузка...</main>;
}
if (isError) {
return (
<main className="container mx-auto py-10">
Ошибка загрузки: {error?.message}
</main>
);
}
return (
<main className="flex-1 flex relative">
<AppSidebar
onAddClick={() => {
setIsAddDialogOpen(true);
}}
onEditClick={() => {
openEditForm();
}}
/>
<div className="flex-1 p-4">
{!selectedItem && (
<DialogForm<PeriodBindingModel>
title="Форма сроков"
description="Добавить сроки"
isOpen={isAddDialogOpen}
onClose={() => setIsAddDialogOpen(false)}
onSubmit={handleAdd}
>
<PeriodFormAdd />
</DialogForm>
)}
{selectedItem && (
<DialogForm<PeriodBindingModel>
title="Форма сроков"
description="Изменить сроки"
isOpen={isEditDialogOpen}
onClose={() => setIsEditDialogOpen(false)}
onSubmit={handleEdit}
>
<PeriodFormEdit defaultValues={selectedItem} />
</DialogForm>
)}
<div>
<DataTable
data={finalData}
columns={columns}
onRowSelected={(id) => handleSelectItem(id)}
selectedRow={selectedItem?.id}
/>
</div>
</div>
</main>
);
};

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { useAuthStore } from '@/store/workerStore';
import { ProfileForm } from '../features/ProfileForm';
import type { StorekeeperBindingModel } from '@/types/types';
import { useStorekeepers } from '@/hooks/useStorekeepers';
import { toast } from 'sonner';
export const Profile = (): React.JSX.Element => {
const { user, updateUser } = useAuthStore();
const { updateStorekeeper, isUpdateError, updateError } = useStorekeepers();
React.useEffect(() => {
if (isUpdateError) {
toast(updateError?.message);
}
}, [isUpdateError, updateError]);
if (!user) {
return (
<main className="container mx-auto py-10">
Загрузка данных пользователя...
</main>
);
}
const handleUpdate = (data: Partial<StorekeeperBindingModel>) => {
console.log(data);
updateUser(data);
updateStorekeeper(data);
};
return (
<main className="container mx-auto py-10">
<h1 className="text-2xl font-bold mb-6">Профиль пользователя</h1>
<ProfileForm defaultValues={user} onSubmit={handleUpdate} />
</main>
);
};

View File

@@ -0,0 +1,20 @@
import React from 'react';
import { ReportSidebar } from '../layout/ReportSidebar';
import { ReportViewer } from '../features/ReportViewer';
export type SelectedReport = 'word' | 'pdf' | 'excel' | undefined;
export const Reports = (): React.JSX.Element => {
const [selectedReport, setSelectedReport] = React.useState<SelectedReport>();
return (
<main className="flex">
<ReportSidebar
onWordClick={() => setSelectedReport('word')}
onPdfClick={() => setSelectedReport('pdf')}
onExcelClick={() => setSelectedReport('excel')}
/>
<ReportViewer selectedReport={selectedReport} />
</main>
);
};

View File

@@ -0,0 +1,63 @@
import React from 'react';
import { DataTable } from '../layout/DataTable';
import type { ColumnDef } from '../layout/DataTable';
import { useStorekeepers } from '@/hooks/useStorekeepers';
import type { StorekeeperBindingModel } from '@/types/types';
const columns: ColumnDef<StorekeeperBindingModel>[] = [
{
accessorKey: 'id',
header: 'ID',
},
{
accessorKey: 'name',
header: 'Имя',
},
{
accessorKey: 'surname',
header: 'Фамилия',
},
{
accessorKey: 'middleName',
header: 'Отчество',
},
{
accessorKey: 'login',
header: 'Логин',
},
{
accessorKey: 'email',
header: 'Email',
},
{
accessorKey: 'phoneNumber',
header: 'Телефон',
},
];
export const Storekeepers = (): React.JSX.Element => {
const { storekeepers, isLoading, error } = useStorekeepers();
if (isLoading) {
return <main className="container mx-auto py-10">Загрузка...</main>;
}
if (error) {
return (
<main className="container mx-auto py-10">
Ошибка загрузки данных: {error.message}
</main>
);
}
return (
<main className="container mx-auto py-10">
<h1 className="text-2xl font-bold mb-6">Кладовщики</h1>
<DataTable
data={storekeepers || []}
columns={columns}
onRowSelected={console.log}
/>
</main>
);
};

View File

@@ -0,0 +1,51 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
)}
{...props}
/>
)
}
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -0,0 +1,59 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

Some files were not shown because too many files have changed in this diff Show More