32 Commits

Author SHA1 Message Date
a690777272 Merge pull request 'Task_7.5_UI_Reports' (#12) from Task_7.5_UI_Reports into main
Reviewed-on: #12
2025-05-27 22:25:08 +04:00
9401c42ffd feat!: первая готовая версия веб интерфейса 2025-05-27 22:24:05 +04:00
6b1f57a3b9 feat: clerk ui 2025-05-27 21:55:13 +04:00
b8a9409dad feat: отчеты для клерка 2025-05-27 18:20:55 +04:00
2b24a8d6f2 fix: id для word и excel кладовщика 2025-05-27 01:40:12 +04:00
0a831f0a6c feat: cdn работа с pdf.js 2025-05-27 01:28:21 +04:00
bde930a544 feat: merge branches reports with prev ui version 2025-05-26 21:19:24 +04:00
b70143484d fix: добавление надписей методов в котроллере 2025-05-25 23:18:20 +04:00
75c0be18ce fix: добавление названий методов 2025-05-25 11:53:41 +04:00
a26193512c feat: добавление выборки для отчетов 2025-05-24 22:49:10 +04:00
6c561fb147 Merge pull request 'Task_7.5_UI_Reports' (#11) from Task_7.5_UI_Reports into main
Reviewed-on: #11
Breakin Change
2025-05-22 12:31:01 +04:00
46a8843a84 feat: кладовщик
Breaking Change
2025-05-22 12:30:04 +04:00
ce492dd8a1 feat: готовый excel 2025-05-22 11:17:16 +04:00
1153b716f4 feat: ебать 2025-05-22 03:59:48 +04:00
b6d3d53856 Merge branch 'Task_7_UI' into Task_7.5_UI_Reports 2025-05-21 23:20:21 +04:00
9138a12e97 fix: убрал word и doc просмотр у кладовщика 2025-05-21 23:03:41 +04:00
ed2369ed85 fix: небольшие фиксы 2025-05-21 22:13:22 +04:00
92d02d4ba6 feat: простенький экран отчетов 2025-05-21 21:16:49 +04:00
b977e76302 Merge branch 'main' into Task_7_UI 2025-05-21 19:24:24 +04:00
4fdc420920 Merge pull request 'Task забыл' (#10) from Task_5_Api into main
Reviewed-on: #10
2025-05-21 19:22:58 +04:00
8e930475a3 feat: clerk ui, частично не рабочий из-за маппингов 2025-05-21 17:58:54 +04:00
57f878a051 feat: начало вьюхи клерка, готовы три таба, ни один не работает нормально, спасибо автомапперу 2025-05-21 17:43:16 +04:00
b59bdf9f3d feat: подготовка ui для клерка 2025-05-21 01:51:45 +04:00
f5d5ff4b24 merge 2025-05-20 22:31:55 +04:00
3e48ad4d24 Merge pull request 'Task_5_Api' (#7) from Task_5_Api into main
Reviewed-on: #7
feat: реализация web api, которая скорее всего работает, но чиниться будет уже в следующих тасках
2025-05-20 13:49:59 +04:00
b1e5b7de93 fix: поправил ошибки на формочке кредитных программ 2025-05-20 00:31:38 +04:00
9ed33690cf feat: первая простенькая версия ui для кладовщика 2025-05-20 00:00:45 +04:00
1c0bf1efd2 feat: немного поменял маппинг, сделал отображение нормальное без View модели с фио, для кладовщика основные формы и создание готово, осталось редактирования 2025-05-19 02:21:13 +04:00
e8f493691f feat: авторизация для кладовщика 2025-05-19 00:13:37 +04:00
de879be266 fix: добавил создание кредитных программ, починил по тупому cors и id на клиенте решил генерить 2025-05-18 02:52:11 +04:00
164def1e18 feat: popup формочка для добавления, хуки для загрузки данных 2025-05-18 02:20:03 +04:00
56053a7287 feat: ui для header и footer 2025-05-17 13:21:33 +04:00
239 changed files with 18885 additions and 5065 deletions

View File

@@ -24,17 +24,30 @@ public class ReportContract(IClientStorageContract clientStorage, ICurrencyStora
private static readonly string[] clientsByDepositHeader = ["Фамилия", "Имя", "Баланс", "Ставка", "Срок", "Период"];
private static readonly string[] currencyHeader = ["Валюта", "Кредитная программа", "Макс. сумма", "Ставка", "Срок"];
public async Task<List<ClientsByCreditProgramDataModel>> GetDataClientsByCreditProgramAsync(CancellationToken ct)
/// <summary>
/// Получения данных для отчета Клиента по Кредитным программам
/// </summary>
/// <param name="creditProgramIds"></param>
/// <param name="ct"></param>
/// <returns></returns>
public async Task<List<ClientsByCreditProgramDataModel>> GetDataClientsByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct)
{
_logger.LogInformation("Get data ClientsByCreditProgram");
if (creditProgramIds is null || creditProgramIds.Count == 0)
{
return [];
}
var clients = await Task.Run(() => _clientStorage.GetList(), ct);
var creditPrograms = await Task.Run(() => _creditProgramStorage.GetList(), ct);
var currencies = await Task.Run(() => _currencyStorage.GetList(), ct);
return creditPrograms
.Where(cp => cp.Currencies.Any()) // Проверяем, что у кредитной программы есть связанные валюты
var filteredPrograms = creditPrograms
.Where(cp => creditProgramIds.Contains(cp.Id));
return filteredPrograms
.Select(cp => new ClientsByCreditProgramDataModel
{
CreditProgramId = cp.Id,
CreditProgramName = cp.Name,
ClientSurname = clients.Where(c => c.CreditProgramClients.Any(cpc => cpc.CreditProgramId == cp.Id))
.Select(c => c.Surname).ToList(),
@@ -45,10 +58,17 @@ public class ReportContract(IClientStorageContract clientStorage, ICurrencyStora
}).ToList();
}
public async Task<Stream> CreateDocumentClientsByCreditProgramAsync(CancellationToken ct)
/// <summary>
/// Создание word отчета Клиента по Кредитным программам
/// </summary>
/// <param name="creditProgramIds"></param>
/// <param name="ct"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<Stream> CreateDocumentClientsByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct)
{
_logger.LogInformation("Create report ClientsByCreditProgram");
var data = await GetDataClientsByCreditProgramAsync(ct) ?? throw new InvalidOperationException("No found data");
var data = await GetDataClientsByCreditProgramAsync(creditProgramIds, ct) ?? throw new InvalidOperationException("No found data");
var tableRows = new List<string[]>
{
@@ -59,27 +79,34 @@ 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 _baseWordBuilder
.AddHeader("Клиенты по кредитным программам")
.AddParagraph($"Сформировано на дату {DateTime.Now}")
.AddTable([3000, 3000, 3000, 3000], tableRows)
.AddTable([2000, 2000, 2000, 2000], tableRows)
.Build();
}
public async Task<Stream> CreateExcelDocumentClientsByCreditProgramAsync(CancellationToken ct)
/// <summary>
/// Создание excel отчета Клиенты по Кредитным программам
/// </summary>
/// <param name="creditProgramIds"></param>
/// <param name="ct"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<Stream> CreateExcelDocumentClientsByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct)
{
_logger.LogInformation("Create Excel report ClientsByCreditProgram");
var data = await GetDataClientsByCreditProgramAsync(ct) ?? throw new InvalidOperationException("No found data");
var data = await GetDataClientsByCreditProgramAsync(creditProgramIds, ct) ?? throw new InvalidOperationException("No found data");
var tableRows = new List<string[]>
{
@@ -90,23 +117,32 @@ 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();
}
/// <summary>
/// Получение данных для отчета Клиента по Депозитам
/// </summary>
/// <param name="dateStart"></param>
/// <param name="dateFinish"></param>
/// <param name="ct"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="InvalidOperationException"></exception>
public async Task<List<ClientsByDepositDataModel>> GetDataClientsByDepositAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct)
{
_logger.LogInformation("Get data ClientsByDeposit from {dateStart} to {dateFinish}", dateStart, dateFinish);
@@ -147,14 +183,23 @@ public class ReportContract(IClientStorageContract clientStorage, ICurrencyStora
}
}
if (!result.Any())
{
throw new InvalidOperationException("No clients with deposits found");
}
//if (!result.Any())
//{
// throw new InvalidOperationException("No clients with deposits found");
//}
return result;
}
/// <summary>
/// Создание pdf отчета Клиента по Депозитам
/// </summary>
/// <param name="dateStart"></param>
/// <param name="dateFinish"></param>
/// <param name="ct"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<Stream> CreateDocumentClientsByDepositAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct)
{
_logger.LogInformation("Create report ClientsByDeposit from {dateStart} to {dateFinish}", dateStart, dateFinish);
@@ -169,24 +214,33 @@ 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();
}
/// <summary>
/// Получение данных для отчета Депозиты и Кредитные программы по Валютам
/// </summary>
/// <param name="dateStart"></param>
/// <param name="dateFinish"></param>
/// <param name="ct"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public async Task<List<CreditProgramAndDepositByCurrencyDataModel>> GetDataDepositAndCreditProgramByCurrencyAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct)
{
_logger.LogInformation("Get data DepositAndCreditProgramByCurrency from {dateStart} to {dateFinish}", dateStart, dateFinish);
@@ -215,6 +269,15 @@ public class ReportContract(IClientStorageContract clientStorage, ICurrencyStora
}).ToList();
}
/// <summary>
/// Создание pdf отчета Депозиты и Кредитные программы по Валютам
/// </summary>
/// <param name="dateStart"></param>
/// <param name="dateFinish"></param>
/// <param name="ct"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<Stream> CreateDocumentDepositAndCreditProgramByCurrencyAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct)
{
_logger.LogInformation("Create report DepositAndCreditProgramByCurrency from {dateStart} to {dateFinish}", dateStart, dateFinish);
@@ -229,27 +292,66 @@ 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();
}
public async Task<List<DepositByCreditProgramDataModel>> GetDataDepositByCreditProgramAsync(CancellationToken ct)
/// <summary>
/// Получение данных для отчета Депозиты по Кредитным программам
/// </summary>
/// <param name="creditProgramIds"></param>
/// <param name="ct"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<List<DepositByCreditProgramDataModel>> GetDataDepositByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct)
{
_logger.LogInformation("Get data DepositByCreditProgram");
var deposits = await Task.Run(() => _depositStorage.GetList(), ct);
@@ -261,7 +363,10 @@ public class ReportContract(IClientStorageContract clientStorage, ICurrencyStora
throw new InvalidOperationException("No deposits with currencies found");
}
return creditPrograms.Select(cp => new DepositByCreditProgramDataModel
var filteredPrograms = creditPrograms
.Where(cp => creditProgramIds == null || creditProgramIds.Contains(cp.Id));
return filteredPrograms.Select(cp => new DepositByCreditProgramDataModel
{
CreditProgramName = cp.Name,
DepositRate = deposits.Select(d => d.InterestRate).ToList(),
@@ -270,10 +375,18 @@ public class ReportContract(IClientStorageContract clientStorage, ICurrencyStora
}).ToList();
}
public async Task<Stream> CreateDocumentDepositByCreditProgramAsync(CancellationToken ct)
/// <summary>
/// Создание word отчета Депозиты по Кредитным программам
/// </summary>
/// <param name="creditProgramIds"></param>
/// <param name="ct"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<Stream> CreateDocumentDepositByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct)
{
_logger.LogInformation("Create report DepositByCreditProgram");
var data = await GetDataDepositByCreditProgramAsync(ct) ?? throw new InvalidOperationException("No found data");
var data = await GetDataDepositByCreditProgramAsync(creditProgramIds, ct) ?? throw new InvalidOperationException("No found data");
var tableRows = new List<string[]>
{
@@ -284,27 +397,34 @@ 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();
}
public async Task<Stream> CreateExcelDocumentDepositByCreditProgramAsync(CancellationToken ct)
/// <summary>
/// Создание excel отчета Депозиты по Кредитным программам
/// </summary>
/// <param name="creditProgramIds"></param>
/// <param name="ct"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<Stream> CreateExcelDocumentDepositByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct)
{
_logger.LogInformation("Create Excel report DepositByCreditProgram");
var data = await GetDataDepositByCreditProgramAsync(ct) ?? throw new InvalidOperationException("No found data");
var data = await GetDataDepositByCreditProgramAsync(creditProgramIds, ct) ?? throw new InvalidOperationException("No found data");
var tableRows = new List<string[]>
{
@@ -315,20 +435,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

@@ -4,15 +4,15 @@ namespace BankContracts.AdapterContracts;
public interface IReportAdapter
{
Task<ReportOperationResponse> GetDataClientsByCreditProgramAsync(CancellationToken ct);
Task<ReportOperationResponse> GetDataClientsByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct);
Task<ReportOperationResponse> GetDataClientsByDepositAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct);
Task<ReportOperationResponse> GetDataDepositByCreditProgramAsync(CancellationToken ct);
Task<ReportOperationResponse> GetDataDepositByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct);
Task<ReportOperationResponse> GetDataDepositAndCreditProgramByCurrencyAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct);
Task<ReportOperationResponse> CreateDocumentClientsByCreditProgramAsync(CancellationToken ct);
Task<ReportOperationResponse> CreateExcelDocumentClientsByCreditProgramAsync(CancellationToken ct);
Task<ReportOperationResponse> CreateDocumentClientsByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct);
Task<ReportOperationResponse> CreateExcelDocumentClientsByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct);
Task<ReportOperationResponse> CreateDocumentClientsByDepositAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct);
Task<ReportOperationResponse> CreateDocumentDepositByCreditProgramAsync(CancellationToken ct);
Task<ReportOperationResponse> CreateExcelDocumentDepositByCreditProgramAsync(CancellationToken ct);
Task<ReportOperationResponse> CreateDocumentDepositByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct);
Task<ReportOperationResponse> CreateExcelDocumentDepositByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct);
Task<ReportOperationResponse> CreateDocumentDepositAndCreditProgramByCurrencyAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct);
}

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

@@ -0,0 +1,18 @@
namespace BankContracts.BindingModels;
public class ReportMailSendInfoBindingModel
{
public string Email { get; set; } = string.Empty;
public string Subject { get; set; } = string.Empty;
public string Body { get; set; } = string.Empty;
}
public class CreditProgramReportMailSendInfoBindingModel : ReportMailSendInfoBindingModel
{
public List<string> CreditProgramIds { get; set; } = new();
}
public class DepositReportMailSendInfoBindingModel : ReportMailSendInfoBindingModel
{
// Для отчетов по депозитам дополнительные поля передаются через query параметры
}

View File

@@ -4,16 +4,16 @@ namespace BankContracts.BusinessLogicContracts;
public interface IReportContract
{
Task<List<ClientsByCreditProgramDataModel>> GetDataClientsByCreditProgramAsync(CancellationToken ct);
Task<Stream> CreateDocumentClientsByCreditProgramAsync(CancellationToken ct);
Task<Stream> CreateExcelDocumentClientsByCreditProgramAsync(CancellationToken ct);
Task<List<ClientsByCreditProgramDataModel>> GetDataClientsByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct);
Task<Stream> CreateDocumentClientsByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct);
Task<Stream> CreateExcelDocumentClientsByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct);
Task<List<ClientsByDepositDataModel>> GetDataClientsByDepositAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct);
Task<Stream> CreateDocumentClientsByDepositAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct);
Task<List<DepositByCreditProgramDataModel>> GetDataDepositByCreditProgramAsync(CancellationToken ct);
Task<Stream> CreateDocumentDepositByCreditProgramAsync(CancellationToken ct);
Task<Stream> CreateExcelDocumentDepositByCreditProgramAsync(CancellationToken ct);
Task<List<DepositByCreditProgramDataModel>> GetDataDepositByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct);
Task<Stream> CreateDocumentDepositByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct);
Task<Stream> CreateExcelDocumentDepositByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct);
Task<List<CreditProgramAndDepositByCurrencyDataModel>> GetDataDepositAndCreditProgramByCurrencyAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct);
Task<Stream> CreateDocumentDepositAndCreditProgramByCurrencyAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct);

View File

@@ -2,6 +2,7 @@
public class ClientsByCreditProgramDataModel
{
public required string CreditProgramId { get; set; }
public required string CreditProgramName { get; set; }
public required List<string> ClientSurname { get; set; }
public required List<string> ClientName { get; set; }

View File

@@ -28,7 +28,7 @@ public class OperationResponse
}
if (Result is Stream stream)
{
return new FileStreamResult(stream, "application/octetstream")
return new FileStreamResult(stream, "application/octet-stream")
{
FileDownloadName = FileName
};
@@ -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

@@ -13,7 +13,7 @@ using System.Text;
namespace BankTests.ReportContractTests;
[TestFixture]
internal class ReportContractTestss
internal class ReportContractTests
{
private ReportContract _reportContract;
private Mock<IClientStorageContract> _clientStorage;
@@ -65,12 +65,37 @@ internal class ReportContractTestss
new DepositDataModel("d1", 5, 1000, 12, "cl", new List<DepositCurrencyDataModel>())
});
var result = await _reportContract.GetDataDepositByCreditProgramAsync(ct);
var result = await _reportContract.GetDataDepositByCreditProgramAsync(null, ct);
Assert.That(result, Is.Not.Null);
Assert.That(result.Count, Is.GreaterThanOrEqualTo(0));
}
[Test]
public async Task GetDataDepositByCreditProgramAsync_WithFilter_ReturnsFilteredData()
{
var ct = CancellationToken.None;
var creditProgramIds = new List<string> { "1" };
_creditProgramStorage.Setup(x => x.GetList(It.IsAny<string>(), It.IsAny<string>()))
.Returns(new List<CreditProgramDataModel>
{
new CreditProgramDataModel("1", "Программа 1", 100, 200, "sk", "p", new List<CreditProgramCurrencyDataModel> { new() { CurrencyId = "1" } }),
new CreditProgramDataModel("2", "Программа 2", 100, 200, "sk", "p", new List<CreditProgramCurrencyDataModel> { new() { CurrencyId = "1" } })
});
_depositStorage.Setup(x => x.GetList(It.IsAny<string>()))
.Returns(new List<DepositDataModel>
{
new DepositDataModel("d1", 5, 1000, 12, "cl", new List<DepositCurrencyDataModel> { new() { CurrencyId = "1" } })
});
var result = await _reportContract.GetDataDepositByCreditProgramAsync(creditProgramIds, ct);
Assert.That(result, Is.Not.Null);
Assert.That(result.Count, Is.EqualTo(1));
Assert.That(result[0].CreditProgramName, Is.EqualTo("Программа 1"));
}
[Test]
public async Task CreateDocumentDepositByCreditProgramAsync_CallsWordBuilder()
{
@@ -78,12 +103,12 @@ internal class ReportContractTestss
_creditProgramStorage.Setup(x => x.GetList(It.IsAny<string>(), It.IsAny<string>()))
.Returns(new List<CreditProgramDataModel>
{
new CreditProgramDataModel("1", "Программа 1", 100, 200, "sk", "p", new List<CreditProgramCurrencyDataModel>())
new CreditProgramDataModel("1", "Программа 1", 100, 200, "sk", "p", new List<CreditProgramCurrencyDataModel> { new() { CurrencyId = "1" } })
});
_depositStorage.Setup(x => x.GetList(It.IsAny<string>()))
.Returns(new List<DepositDataModel>
{
new DepositDataModel("d1", 5, 1000, 12, "cl", new List<DepositCurrencyDataModel>())
new DepositDataModel("d1", 5, 1000, 12, "cl", new List<DepositCurrencyDataModel> { new() { CurrencyId = "1" } })
});
_baseWordBuilder.Setup(x => x.AddHeader(It.IsAny<string>())).Returns(_baseWordBuilder.Object);
@@ -91,7 +116,7 @@ internal class ReportContractTestss
_baseWordBuilder.Setup(x => x.AddTable(It.IsAny<int[]>(), It.IsAny<List<string[]>>())).Returns(_baseWordBuilder.Object);
_baseWordBuilder.Setup(x => x.Build()).Returns(new MemoryStream(Encoding.UTF8.GetBytes("test")));
var stream = await _reportContract.CreateDocumentDepositByCreditProgramAsync(ct);
var stream = await _reportContract.CreateDocumentDepositByCreditProgramAsync(new List<string> { "1" }, ct);
Assert.That(stream, Is.Not.Null);
_baseWordBuilder.Verify(x => x.AddHeader(It.IsAny<string>()), Times.Once);
@@ -163,6 +188,35 @@ internal class ReportContractTestss
[Test]
public async Task CreateExcelDocumentDepositByCreditProgramAsync_CallsExcelBuilder()
{
var ct = CancellationToken.None;
_creditProgramStorage.Setup(x => x.GetList(It.IsAny<string>(), It.IsAny<string>()))
.Returns(new List<CreditProgramDataModel>
{
new CreditProgramDataModel("1", "Программа 1", 100, 200, "sk", "p", new List<CreditProgramCurrencyDataModel> { new() { CurrencyId = "1" } })
});
_depositStorage.Setup(x => x.GetList(It.IsAny<string>()))
.Returns(new List<DepositDataModel>
{
new DepositDataModel("d1", 5, 1000, 12, "cl", new List<DepositCurrencyDataModel> { new() { CurrencyId = "1" } })
});
_baseExcelBuilder.Setup(x => x.AddHeader(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>())).Returns(_baseExcelBuilder.Object);
_baseExcelBuilder.Setup(x => x.AddParagraph(It.IsAny<string>(), It.IsAny<int>())).Returns(_baseExcelBuilder.Object);
_baseExcelBuilder.Setup(x => x.AddTable(It.IsAny<int[]>(), It.IsAny<List<string[]>>())).Returns(_baseExcelBuilder.Object);
_baseExcelBuilder.Setup(x => x.Build()).Returns(new MemoryStream(Encoding.UTF8.GetBytes("test")));
var stream = await _reportContract.CreateExcelDocumentDepositByCreditProgramAsync(new List<string> { "1" }, ct);
Assert.That(stream, Is.Not.Null);
_baseExcelBuilder.Verify(x => x.AddHeader(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()), Times.Once);
_baseExcelBuilder.Verify(x => x.AddParagraph(It.IsAny<string>(), It.IsAny<int>()), Times.Once);
_baseExcelBuilder.Verify(x => x.AddTable(It.IsAny<int[]>(), It.IsAny<List<string[]>>()), Times.Once);
_baseExcelBuilder.Verify(x => x.Build(), Times.Once);
}
[Test]
public async Task GetDataDepositByCreditProgramAsync_NoCurrencies_ThrowsException()
{
var ct = CancellationToken.None;
_creditProgramStorage.Setup(x => x.GetList(It.IsAny<string>(), It.IsAny<string>()))
@@ -176,17 +230,7 @@ internal class ReportContractTestss
new DepositDataModel("d1", 5, 1000, 12, "cl", new List<DepositCurrencyDataModel>())
});
_baseExcelBuilder.Setup(x => x.AddHeader(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>())).Returns(_baseExcelBuilder.Object);
_baseExcelBuilder.Setup(x => x.AddParagraph(It.IsAny<string>(), It.IsAny<int>())).Returns(_baseExcelBuilder.Object);
_baseExcelBuilder.Setup(x => x.AddTable(It.IsAny<int[]>(), It.IsAny<List<string[]>>())).Returns(_baseExcelBuilder.Object);
_baseExcelBuilder.Setup(x => x.Build()).Returns(new MemoryStream(Encoding.UTF8.GetBytes("test")));
var stream = await _reportContract.CreateExcelDocumentDepositByCreditProgramAsync(ct);
Assert.That(stream, Is.Not.Null);
_baseExcelBuilder.Verify(x => x.AddHeader(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()), Times.Once);
_baseExcelBuilder.Verify(x => x.AddParagraph(It.IsAny<string>(), It.IsAny<int>()), Times.Once);
_baseExcelBuilder.Verify(x => x.AddTable(It.IsAny<int[]>(), It.IsAny<List<string[]>>()), Times.Once);
_baseExcelBuilder.Verify(x => x.Build(), Times.Once);
Assert.ThrowsAsync<InvalidOperationException>(() =>
_reportContract.GetDataDepositByCreditProgramAsync(null, ct));
}
}

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

@@ -36,16 +36,12 @@ public class ReportAdapter : IReportAdapter
return ReportOperationResponse.OK(stream, fileName);
}
public async Task<ReportOperationResponse> CreateDocumentClientsByCreditProgramAsync(CancellationToken ct)
public async Task<ReportOperationResponse> CreateDocumentClientsByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct)
{
try
{
return SendStream(await _reportContract.CreateDocumentClientsByCreditProgramAsync(ct), "clientbycreditprogram.docx");
}
catch (IncorrectDatesException ex)
{
_logger.LogError(ex, "IncorrectDatesException");
return ReportOperationResponse.BadRequest($"Incorrect dates: {ex.Message}");
return SendStream(await _reportContract.CreateDocumentClientsByCreditProgramAsync(creditProgramIds, ct),
"clientsbycreditprogram.docx");
}
catch (InvalidOperationException ex)
{
@@ -60,21 +56,16 @@ public class ReportAdapter : IReportAdapter
catch (Exception ex)
{
_logger.LogError(ex, "Exception");
return
ReportOperationResponse.InternalServerError(ex.Message);
return ReportOperationResponse.InternalServerError(ex.Message);
}
}
public async Task<ReportOperationResponse> CreateExcelDocumentClientsByCreditProgramAsync(CancellationToken ct)
public async Task<ReportOperationResponse> CreateExcelDocumentClientsByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct)
{
try
{
return SendStream(await _reportContract.CreateExcelDocumentClientsByCreditProgramAsync(ct), "clientbycreditprogram.xlsx");
}
catch (IncorrectDatesException ex)
{
_logger.LogError(ex, "IncorrectDatesException");
return ReportOperationResponse.BadRequest($"Incorrect dates: {ex.Message}");
return SendStream(await _reportContract.CreateExcelDocumentClientsByCreditProgramAsync(creditProgramIds, ct),
"clientsbycreditprogram.xlsx");
}
catch (InvalidOperationException ex)
{
@@ -89,8 +80,7 @@ public class ReportAdapter : IReportAdapter
catch (Exception ex)
{
_logger.LogError(ex, "Exception");
return
ReportOperationResponse.InternalServerError(ex.Message);
return ReportOperationResponse.InternalServerError(ex.Message);
}
}
@@ -98,8 +88,7 @@ public class ReportAdapter : IReportAdapter
{
try
{
return SendStream(await _reportContract.CreateDocumentClientsByDepositAsync(dateStart, dateFinish, ct),
"clientbydeposit.pdf");
return SendStream(await _reportContract.CreateDocumentClientsByDepositAsync(dateStart, dateFinish, ct), "clientbydeposit.pdf");
}
catch (IncorrectDatesException ex)
{
@@ -154,11 +143,11 @@ public class ReportAdapter : IReportAdapter
}
}
public async Task<ReportOperationResponse> CreateDocumentDepositByCreditProgramAsync(CancellationToken ct)
public async Task<ReportOperationResponse> CreateDocumentDepositByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct)
{
try
{
return SendStream(await _reportContract.CreateDocumentDepositByCreditProgramAsync(ct), "depositbycreditprogram.docx");
return SendStream(await _reportContract.CreateDocumentDepositByCreditProgramAsync(creditProgramIds, ct), "depositbycreditprogram.docx");
}
catch (IncorrectDatesException ex)
{
@@ -187,11 +176,11 @@ public class ReportAdapter : IReportAdapter
}
}
public async Task<ReportOperationResponse> CreateExcelDocumentDepositByCreditProgramAsync(CancellationToken ct)
public async Task<ReportOperationResponse> CreateExcelDocumentDepositByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct)
{
try
{
return SendStream(await _reportContract.CreateExcelDocumentDepositByCreditProgramAsync(ct), "depositbycreditprogram.xlsx");
return SendStream(await _reportContract.CreateExcelDocumentDepositByCreditProgramAsync(creditProgramIds, ct), "depositbycreditprogram.xlsx");
}
catch (IncorrectDatesException ex)
{
@@ -216,21 +205,16 @@ public class ReportAdapter : IReportAdapter
catch (Exception ex)
{
_logger.LogError(ex, "Exception");
return
ReportOperationResponse.InternalServerError(ex.Message);
return ReportOperationResponse.InternalServerError(ex.Message);
}
}
public async Task<ReportOperationResponse> GetDataClientsByCreditProgramAsync(CancellationToken ct)
public async Task<ReportOperationResponse> GetDataClientsByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct)
{
try
{
return ReportOperationResponse.OK([.. (await _reportContract.GetDataClientsByCreditProgramAsync(ct)).Select(x => _mapper.Map<ClientsByCreditProgramViewModel>(x))]);
}
catch (IncorrectDatesException ex)
{
_logger.LogError(ex, "IncorrectDatesException");
return ReportOperationResponse.BadRequest($"Incorrect dates: {ex.Message}");
return ReportOperationResponse.OK((await _reportContract.GetDataClientsByCreditProgramAsync(creditProgramIds, ct))
.Select(x => _mapper.Map<ClientsByCreditProgramViewModel>(x)).ToList());
}
catch (InvalidOperationException ex)
{
@@ -245,8 +229,7 @@ public class ReportAdapter : IReportAdapter
catch (Exception ex)
{
_logger.LogError(ex, "Exception");
return
ReportOperationResponse.InternalServerError(ex.Message);
return ReportOperationResponse.InternalServerError(ex.Message);
}
}
@@ -310,11 +293,11 @@ public class ReportAdapter : IReportAdapter
}
}
public async Task<ReportOperationResponse> GetDataDepositByCreditProgramAsync(CancellationToken ct)
public async Task<ReportOperationResponse> GetDataDepositByCreditProgramAsync(List<string>? creditProgramIds, CancellationToken ct)
{
try
{
return ReportOperationResponse.OK([.. (await _reportContract.GetDataDepositByCreditProgramAsync(ct)).Select(x => _mapper.Map<DepositByCreditProgramViewModel>(x))]);
return ReportOperationResponse.OK([.. (await _reportContract.GetDataDepositByCreditProgramAsync(creditProgramIds, ct)).Select(x => _mapper.Map<DepositByCreditProgramViewModel>(x))]);
}
catch (IncorrectDatesException ex)
{
@@ -334,8 +317,7 @@ public class ReportAdapter : IReportAdapter
catch (Exception ex)
{
_logger.LogError(ex, "Exception");
return
ReportOperationResponse.InternalServerError(ex.Message);
return ReportOperationResponse.InternalServerError(ex.Message);
}
}
}

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,42 +9,56 @@ 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();
/// <summary>
/// Получение данных Клиента по Кредитным программам
/// </summary>
/// <param name="creditProgramIds"></param>
/// <param name="ct"></param>
/// <returns></returns>
[HttpGet]
[Consumes("application/json")]
public async Task<IActionResult> GetClientByCreditProgram(CancellationToken ct)
public async Task<IActionResult> GetClientByCreditProgram([FromQuery] List<string>? creditProgramIds, CancellationToken ct)
{
return (await
_adapter.GetDataClientsByCreditProgramAsync(ct)).GetResponse(Request, Response);
return (await _adapter.GetDataClientsByCreditProgramAsync(creditProgramIds, ct)).GetResponse(Request, Response);
}
/// <summary>
/// Отчет word Клиента по Кредитным программам
/// </summary>
/// <param name="creditProgramIds"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet]
[Consumes("application/octet-stream")]
public async Task<IActionResult> LoadClientsByCreditProgram(CancellationToken cancellationToken)
public async Task<IActionResult> LoadClientsByCreditProgram([FromQuery] List<string>? creditProgramIds, CancellationToken cancellationToken)
{
return (await
_adapter.CreateDocumentClientsByCreditProgramAsync(cancellationToken)).GetResponse(Request, Response);
return (await _adapter.CreateDocumentClientsByCreditProgramAsync(creditProgramIds, cancellationToken)).GetResponse(Request, Response);
}
/// <summary>
/// Отчет excel Клиента по Кредитным программам
/// </summary>
/// <param name="creditProgramIds"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet]
[Consumes("application/octet-stream")]
public async Task<IActionResult> LoadExcelClientByCreditProgram(CancellationToken cancellationToken)
public async Task<IActionResult> LoadExcelClientByCreditProgram([FromQuery] List<string>? creditProgramIds, CancellationToken cancellationToken)
{
return (await
_adapter.CreateExcelDocumentClientsByCreditProgramAsync(cancellationToken)).GetResponse(Request, Response);
return (await _adapter.CreateExcelDocumentClientsByCreditProgramAsync(creditProgramIds, cancellationToken)).GetResponse(Request, Response);
}
/// <summary>
/// Получение данных Клиента по Вкладам
/// </summary>
/// <param name="fromDate"></param>
/// <param name="toDate"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet]
[Consumes("application/json")]
public async Task<IActionResult> GetClientByDeposit(DateTime fromDate, DateTime toDate, CancellationToken cancellationToken)
@@ -51,43 +66,82 @@ public class ReportController : ControllerBase
return (await _adapter.GetDataClientsByDepositAsync(fromDate, toDate, cancellationToken)).GetResponse(Request, Response);
}
/// <summary>
/// Отчет word Клиента по Вкладам
/// </summary>
/// <param name="fromDate"></param>
/// <param name="toDate"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet]
[Consumes("application/octet-stream")]
public async Task<IActionResult> LoadClientsByDeposit(DateTime fromDate, DateTime toDate, CancellationToken cancellationToken)
{
return (await _adapter.CreateDocumentClientsByDepositAsync(fromDate,
toDate, cancellationToken)).GetResponse(Request, Response);
return (await _adapter.CreateDocumentClientsByDepositAsync(fromDate, toDate, cancellationToken)).GetResponse(Request, Response);
}
/// <summary>
/// Получение данных Вклада по Кредитным программам
/// </summary>
/// <param name="creditProgramIds"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet]
[Consumes("application/json")]
public async Task<IActionResult> GetDepositByCreditProgram(CancellationToken cancellationToken)
public async Task<IActionResult> GetDepositByCreditProgram([FromQuery] List<string>? creditProgramIds, CancellationToken cancellationToken)
{
return (await _adapter.GetDataDepositByCreditProgramAsync(cancellationToken)).GetResponse(Request, Response);
return (await _adapter.GetDataDepositByCreditProgramAsync(creditProgramIds, cancellationToken)).GetResponse(Request, Response);
}
/// <summary>
/// Отчет word Вклада по Кредитным программам
/// </summary>
/// <param name="creditProgramIds"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet]
[Consumes("application/octet-stream")]
public async Task<IActionResult> LoadDepositByCreditProgram(CancellationToken cancellationToken)
public async Task<IActionResult> LoadDepositByCreditProgram([FromQuery] List<string>? creditProgramIds, CancellationToken cancellationToken)
{
return (await _adapter.CreateDocumentDepositByCreditProgramAsync(cancellationToken)).GetResponse(Request, Response);
return (await _adapter.CreateDocumentDepositByCreditProgramAsync(creditProgramIds, cancellationToken)).GetResponse(Request, Response);
}
/// <summary>
/// Отчет excel Вклада по Кредитным программам
/// </summary>
/// <param name="creditProgramIds"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet]
[Consumes("application/octet-stream")]
public async Task<IActionResult> LoadExcelDepositByCreditProgram(CancellationToken cancellationToken)
public async Task<IActionResult> LoadExcelDepositByCreditProgram([FromQuery] List<string>? creditProgramIds, CancellationToken cancellationToken)
{
return (await _adapter.CreateExcelDocumentDepositByCreditProgramAsync(cancellationToken)).GetResponse(Request, Response);
return (await _adapter.CreateExcelDocumentDepositByCreditProgramAsync(creditProgramIds, cancellationToken)).GetResponse(Request, Response);
}
/// <summary>
/// Получение данных Вклада и Кредитных программам по Валютам
/// кладовщик pdf
/// </summary>
/// <param name="fromDate"></param>
/// <param name="toDate"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet]
[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);
}
/// <summary>
/// Отчет pdf Вклада и Кредитных программам по Валютам
/// кладовщик pdf
/// </summary>
/// <param name="fromDate"></param>
/// <param name="toDate"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet]
[Consumes("application/octet-stream")]
public async Task<IActionResult> LoadDepositAndCreditProgramByCurrency(DateTime fromDate, DateTime toDate, CancellationToken cancellationToken)
@@ -95,30 +149,39 @@ public class ReportController : ControllerBase
return (await _adapter.CreateDocumentDepositAndCreditProgramByCurrencyAsync(fromDate, toDate, cancellationToken)).GetResponse(Request, Response);
}
/// <summary>
/// Отправка word отчета Клиентов по Кредитным программам
/// </summary>
/// <param name="mailInfo"></param>
/// <param name="ct"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> SendReportByCreditProgram(string email, CancellationToken ct)
public async Task<IActionResult> SendReportByCreditProgram([FromBody] CreditProgramReportMailSendInfoBindingModel mailInfo, CancellationToken ct)
{
try
{
var report = await _adapter.CreateDocumentClientsByCreditProgramAsync(ct);
var report = await _adapter.CreateDocumentClientsByCreditProgramAsync(mailInfo.CreditProgramIds, ct);
var response = report.GetResponse(Request, Response);
if (response is FileStreamResult fileResult)
{
var tempPath = Path.GetTempFileName();
using (var fileStream = new FileStream(tempPath, FileMode.Create))
var tempPathWithExtension = Path.ChangeExtension(tempPath, ".docx");
using (var fileStream = new FileStream(tempPathWithExtension, FileMode.Create))
{
await fileResult.FileStream.CopyToAsync(fileStream);
}
await _emailService.SendReportAsync(
toEmail: email,
subject: "Отчет по клиентам по кредитным программам",
body: "<h1>Отчет по клиентам по кредитным программам</h1><p>В приложении находится отчет по клиентам по кредитным программам.</p>",
attachmentPath: tempPath
toEmail: mailInfo.Email,
subject: mailInfo.Subject,
body: mailInfo.Body,
attachmentPath: tempPathWithExtension
);
System.IO.File.Delete(tempPath);
System.IO.File.Delete(tempPathWithExtension);
return Ok("Отчет успешно отправлен на почту");
}
@@ -130,30 +193,41 @@ public class ReportController : ControllerBase
}
}
/// <summary>
/// Отправка pdf отчета Клиентов по Депозитам
/// </summary>
/// <param name="mailInfo"></param>
/// <param name="fromDate"></param>
/// <param name="toDate"></param>
/// <param name="ct"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> SendReportByDeposit(string email, DateTime fromDate, DateTime toDate, CancellationToken ct)
public async Task<IActionResult> SendReportByDeposit([FromBody] DepositReportMailSendInfoBindingModel mailInfo, 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 tempPath = Path.GetTempFileName();
using (var fileStream = new FileStream(tempPath, FileMode.Create))
var tempPathWithExtension = Path.ChangeExtension(tempPath, ".pdf");
using (var fileStream = new FileStream(tempPathWithExtension, 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
toEmail: mailInfo.Email,
subject: mailInfo.Subject,
body: mailInfo.Body,
attachmentPath: tempPathWithExtension
);
System.IO.File.Delete(tempPath);
System.IO.File.Delete(tempPathWithExtension);
return Ok("Отчет успешно отправлен на почту");
}
@@ -165,30 +239,42 @@ public class ReportController : ControllerBase
}
}
/// <summary>
/// Отправка pdf отчета Вкладов и Кредитных программ по Валютам
/// кладовщик pdf
/// </summary>
/// <param name="mailInfo"></param>
/// <param name="fromDate"></param>
/// <param name="toDate"></param>
/// <param name="ct"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> SendReportByCurrency(string email, DateTime fromDate, DateTime toDate, CancellationToken ct)
public async Task<IActionResult> SendReportByCurrency([FromBody] DepositReportMailSendInfoBindingModel mailInfo, 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 tempPath = Path.GetTempFileName();
using (var fileStream = new FileStream(tempPath, FileMode.Create))
var tempPathWithExtension = Path.ChangeExtension(tempPath, ".pdf");
using (var fileStream = new FileStream(tempPathWithExtension, 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
toEmail: mailInfo.Email,
subject: mailInfo.Subject,
body: mailInfo.Body,
attachmentPath: tempPathWithExtension
);
System.IO.File.Delete(tempPath);
System.IO.File.Delete(tempPathWithExtension);
return Ok("Отчет успешно отправлен на почту");
}
@@ -200,30 +286,39 @@ public class ReportController : ControllerBase
}
}
/// <summary>
/// Отправка excel отчета Клиентов по Кредитным программам
/// </summary>
/// <param name="mailInfo"></param>
/// <param name="ct"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> SendExcelReportByCreditProgram(string email, CancellationToken ct)
public async Task<IActionResult> SendExcelReportByCreditProgram([FromBody] CreditProgramReportMailSendInfoBindingModel mailInfo, CancellationToken ct)
{
try
{
var report = await _adapter.CreateExcelDocumentClientsByCreditProgramAsync(ct);
var report = await _adapter.CreateExcelDocumentClientsByCreditProgramAsync(mailInfo.CreditProgramIds, ct);
var response = report.GetResponse(Request, Response);
if (response is FileStreamResult fileResult)
{
var tempPath = Path.GetTempFileName();
using (var fileStream = new FileStream(tempPath, FileMode.Create))
var tempPathWithExtension = Path.ChangeExtension(tempPath, ".xlsx");
using (var fileStream = new FileStream(tempPathWithExtension, FileMode.Create))
{
await fileResult.FileStream.CopyToAsync(fileStream);
}
await _emailService.SendReportAsync(
toEmail: email,
subject: "Excel отчет по клиентам по кредитным программам",
body: "<h1>Excel отчет по клиентам по кредитным программам</h1><p>В приложении находится Excel отчет по клиентам по кредитным программам.</p>",
attachmentPath: tempPath
toEmail: mailInfo.Email,
subject: mailInfo.Subject,
body: mailInfo.Body,
attachmentPath: tempPathWithExtension
);
System.IO.File.Delete(tempPath);
System.IO.File.Delete(tempPathWithExtension);
return Ok("Excel отчет успешно отправлен на почту");
}
@@ -235,30 +330,39 @@ public class ReportController : ControllerBase
}
}
/// <summary>
/// Отправка word отчета Вкладов по Кредитных программ
/// </summary>
/// <param name="mailInfo"></param>
/// <param name="ct"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> SendReportDepositByCreditProgram(string email, CancellationToken ct)
public async Task<IActionResult> SendReportDepositByCreditProgram([FromBody] CreditProgramReportMailSendInfoBindingModel mailInfo, CancellationToken ct)
{
try
{
var report = await _adapter.CreateDocumentDepositByCreditProgramAsync(ct);
var report = await _adapter.CreateDocumentDepositByCreditProgramAsync(mailInfo.CreditProgramIds, ct);
var response = report.GetResponse(Request, Response);
if (response is FileStreamResult fileResult)
{
var tempPath = Path.GetTempFileName();
using (var fileStream = new FileStream(tempPath, FileMode.Create))
var tempPathWithExtension = Path.ChangeExtension(tempPath, ".docx");
using (var fileStream = new FileStream(tempPathWithExtension, FileMode.Create))
{
await fileResult.FileStream.CopyToAsync(fileStream);
}
await _emailService.SendReportAsync(
toEmail: email,
subject: "Отчет по вкладам по кредитным программам",
body: "<h1>Отчет по вкладам по кредитным программам</h1><p>В приложении находится отчет по вкладам по кредитным программам.</p>",
attachmentPath: tempPath
toEmail: mailInfo.Email,
subject: mailInfo.Subject,
body: mailInfo.Body,
attachmentPath: tempPathWithExtension
);
System.IO.File.Delete(tempPath);
System.IO.File.Delete(tempPathWithExtension);
return Ok("Отчет успешно отправлен на почту");
}
@@ -270,30 +374,39 @@ public class ReportController : ControllerBase
}
}
/// <summary>
/// Отправка excel отчета Вкладов по Кредитных программ
/// </summary>
/// <param name="mailInfo"></param>
/// <param name="ct"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> SendExcelReportDepositByCreditProgram(string email, CancellationToken ct)
public async Task<IActionResult> SendExcelReportDepositByCreditProgram([FromBody] CreditProgramReportMailSendInfoBindingModel mailInfo, CancellationToken ct)
{
try
{
var report = await _adapter.CreateExcelDocumentDepositByCreditProgramAsync(ct);
var report = await _adapter.CreateExcelDocumentDepositByCreditProgramAsync(mailInfo.CreditProgramIds, ct);
var response = report.GetResponse(Request, Response);
if (response is FileStreamResult fileResult)
{
var tempPath = Path.GetTempFileName();
using (var fileStream = new FileStream(tempPath, FileMode.Create))
var tempPathWithExtension = Path.ChangeExtension(tempPath, ".xlsx");
using (var fileStream = new FileStream(tempPathWithExtension, FileMode.Create))
{
await fileResult.FileStream.CopyToAsync(fileStream);
}
await _emailService.SendReportAsync(
toEmail: email,
subject: "Excel отчет по вкладам по кредитным программам",
body: "<h1>Excel отчет по вкладам по кредитным программам</h1><p>В приложении находится Excel отчет по вкладам по кредитным программам.</p>",
attachmentPath: tempPath
toEmail: mailInfo.Email,
subject: mailInfo.Subject,
body: mailInfo.Body,
attachmentPath: tempPathWithExtension
);
System.IO.File.Delete(tempPath);
System.IO.File.Delete(tempPathWithExtension);
return Ok("Excel отчет успешно отправлен на почту");
}

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,74 @@
### <20><><EFBFBD><EFBFBD><EFBFBD> Word <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
GET /api/Report/LoadClientsByCreditProgram?creditProgramIds={{creditProgramIds}} HTTP/1.1
Host: localhost
Content-Type: application/octet-stream
Authorization: Bearer {{token}}
# <20><><EFBFBD><EFBFBD>: CreditProgramName, ClientSurname, ClientName, ClientBalance
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Word-<2D><><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>.
### <20><><EFBFBD><EFBFBD><EFBFBD> Excel <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
GET /api/Report/LoadExcelClientByCreditProgram?creditProgramIds={{creditProgramIds}} HTTP/1.1
Host: localhost
Content-Type: application/octet-stream
Authorization: Bearer {{token}}
# <20><><EFBFBD><EFBFBD>: CreditProgramName, ClientSurname, ClientName, ClientBalance
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Excel-<2D><><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>.
### <20><><EFBFBD><EFBFBD><EFBFBD> PDF <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
GET /api/Report/LoadClientsByDeposit?fromDate={{fromDate}}&toDate={{toDate}} HTTP/1.1
Host: localhost
Content-Type: application/octet-stream
Authorization: Bearer {{token}}
# <20><><EFBFBD><EFBFBD>: ClientSurname, ClientName, ClientBalance, DepositRate, DepositPeriod, FromPeriod, ToPeriod
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> PDF-<2D><><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> currencyIds <20> GetDataClientsByDepositAsync.
### <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (JSON)
GET /api/Report/GetClientByCreditProgram?creditProgramIds={{creditProgramIds}} HTTP/1.1
Host: localhost
Content-Type: application/json
Authorization: Bearer {{token}}
# <20><><EFBFBD><EFBFBD>: CreditProgramId, CreditProgramName, ClientSurname, ClientName, ClientBalance
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>.
### <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (JSON)
GET /api/Report/GetClientByDeposit?fromDate={{fromDate}}&toDate={{toDate}} HTTP/1.1
Host: localhost
Content-Type: application/json
Authorization: Bearer {{token}}
# <20><><EFBFBD><EFBFBD>: ClientSurname, ClientName, ClientBalance, DepositRate, DepositPeriod, FromPeriod, ToPeriod
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> currencyIds <20> GetDataClientsByDepositAsync.
### <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Word <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> email: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
POST /api/Report/SendReportByCreditProgram HTTP/1.1
Host: localhost
Content-Type: application/json
Authorization: Bearer {{token}}
{
"email": "{{email}}",
"creditProgramIds": {{creditProgramIds}}
}
# <20><><EFBFBD><EFBFBD>: CreditProgramName, ClientSurname, ClientName, ClientBalance
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Word-<2D><><EFBFBD><EFBFBD><EFBFBD> <20><> email.
### <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Excel <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> email: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
POST /api/Report/SendExcelReportByCreditProgram HTTP/1.1
Host: localhost
Content-Type: application/json
Authorization: Bearer {{token}}
{
"email": "{{email}}",
"creditProgramIds": {{creditProgramIds}}
}
# <20><><EFBFBD><EFBFBD>: CreditProgramName, ClientSurname, ClientName, ClientBalance
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Excel-<2D><><EFBFBD><EFBFBD><EFBFBD> <20><> email.
### <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> PDF <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> email: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
POST /api/Report/SendReportByDeposit?fromDate={{fromDate}}&toDate={{toDate}} HTTP/1.1
Host: localhost
Content-Type: application/json
Authorization: Bearer {{token}}
{
"email": "{{email}}"
}
# <20><><EFBFBD><EFBFBD>: ClientSurname, ClientName, ClientBalance, DepositRate, DepositPeriod, FromPeriod, ToPeriod
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> PDF-<2D><><EFBFBD><EFBFBD><EFBFBD> <20><> email. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> currencyIds <20> GetDataClientsByDepositAsync.

View File

@@ -0,0 +1,76 @@
```
### <20><><EFBFBD><EFBFBD><EFBFBD> Word <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
GET /api/Report/LoadDepositByCreditProgram?creditProgramIds={{creditProgramIds}} HTTP/1.1
Host: localhost
Content-Type: application/octet-stream
Authorization: Bearer {{token}}
# <20><><EFBFBD><EFBFBD>: CreditProgramName, DepositRate, DepositCost, DepositPeriod
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Word-<2D><><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> currencyIds <20> GetDataDepositByCreditProgramAsync.
### <20><><EFBFBD><EFBFBD><EFBFBD> Excel <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
GET /api/Report/LoadExcelDepositByCreditProgram?creditProgramIds={{creditProgramIds}} HTTP/1.1
Host: localhost
Content-Type: application/octet-stream
Authorization: Bearer {{token}}
# <20><><EFBFBD><EFBFBD>: CreditProgramName, DepositRate, DepositCost, DepositPeriod
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Excel-<2D><><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> currencyIds <20> GetDataDepositByCreditProgramAsync.
### <20><><EFBFBD><EFBFBD><EFBFBD> PDF <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
GET /api/Report/LoadDepositAndCreditProgramByCurrency?fromDate={{fromDate}}&toDate={{toDate}} HTTP/1.1
Host: localhost
Content-Type: application/octet-stream
Authorization: Bearer {{token}}
# <20><><EFBFBD><EFBFBD>: CurrencyName, CreditProgramName, CreditProgramMaxCost, DepositRate, DepositPeriod, FromPeriod, ToPeriod
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> PDF-<2D><><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> currencyIds <20> GetDataDepositAndCreditProgramByCurrencyAsync.
### <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (JSON)
GET /api/Report/GetDepositByCreditProgram?creditProgramIds={{creditProgramIds}} HTTP/1.1
Host: localhost
Content-Type: application/json
Authorization: Bearer {{token}}
# <20><><EFBFBD><EFBFBD>: CreditProgramName, DepositRate, DepositCost, DepositPeriod
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> currencyIds <20> GetDataDepositByCreditProgramAsync.
### <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (JSON)
GET /api/Report/GetDepositAndCreditProgramByCurrency?fromDate={{fromDate}}&toDate={{toDate}} HTTP/1.1
Host: localhost
Content-Type: application/json
Authorization: Bearer {{token}}
# <20><><EFBFBD><EFBFBD>: CurrencyName, CreditProgramName, CreditProgramMaxCost, DepositRate, DepositPeriod, FromPeriod, ToPeriod
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> currencyIds <20> GetDataDepositAndCreditProgramByCurrencyAsync.
### <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Word <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> email: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
POST /api/Report/SendReportDepositByCreditProgram HTTP/1.1
Host: localhost
Content-Type: application/json
Authorization: Bearer {{token}}
{
"email": "{{email}}",
"creditProgramIds": {{creditProgramIds}}
}
# <20><><EFBFBD><EFBFBD>: CreditProgramName, DepositRate, DepositCost, DepositPeriod
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Word-<2D><><EFBFBD><EFBFBD><EFBFBD> <20><> email. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> currencyIds <20> GetDataDepositByCreditProgramAsync.
### <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Excel <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> email: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
POST /api/Report/SendExcelReportDepositByCreditProgram HTTP/1.1
Host: localhost
Content-Type: application/json
Authorization: Bearer {{token}}
{
"email": "{{email}}",
"creditProgramIds": {{creditProgramIds}}
}
# <20><><EFBFBD><EFBFBD>: CreditProgramName, DepositRate, DepositCost, DepositPeriod
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Excel-<2D><><EFBFBD><EFBFBD><EFBFBD> <20><> email. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> currencyIds <20> GetDataDepositByCreditProgramAsync.
### <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> PDF <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> email: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
POST /api/Report/SendReportByCurrency?fromDate={{fromDate}}&toDate={{toDate}} HTTP/1.1
Host: localhost
Content-Type: application/json
Authorization: Bearer {{token}}
{
"email": "{{email}}"
}
# <20><><EFBFBD><EFBFBD>: CurrencyName, CreditProgramName, CreditProgramMaxCost, DepositRate, DepositPeriod, FromPeriod, ToPeriod
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> PDF-<2D><><EFBFBD><EFBFBD><EFBFBD> <20><> email. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> currencyIds <20> GetDataDepositAndCreditProgramByCurrencyAsync.
```

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

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

@@ -0,0 +1,26 @@
# 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?
package-lock.json

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,63 @@
{
"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.14",
"@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-radio-group": "^1.3.7",
"@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-separator": "^1.1.6",
"@radix-ui/react-slot": "^1.2.3",
"@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",
"cmdk": "^1.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

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,162 @@
import { ConfigManager } from '@/lib/config';
import type {
CreditProgramReportMailSendInfoBindingModel,
DepositReportMailSendInfoBindingModel,
} from '@/types/types';
const API_URL = ConfigManager.loadUrl();
export const reportsApi = {
// PDF отчеты
getPdfReport: async (fromDate: string, toDate: string) => {
const res = await fetch(
`${API_URL}/api/Report/LoadDepositAndCreditProgramByCurrency?fromDate=${fromDate}&toDate=${toDate}`,
{
credentials: 'include',
headers: {
'Content-Type': 'application/octet-stream',
},
},
);
if (!res.ok) {
throw new Error(`Не удалось загрузить PDF отчет: ${res.statusText}`);
}
return res.blob();
},
sendPdfReportByEmail: async (
mailInfo: DepositReportMailSendInfoBindingModel,
fromDate: string,
toDate: string,
) => {
const res = await fetch(
`${API_URL}/api/Report/SendReportByCurrency?fromDate=${fromDate}&toDate=${toDate}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(mailInfo),
},
);
if (!res.ok) {
throw new Error(`Не удалось отправить PDF отчет: ${res.statusText}`);
}
},
// Word отчеты
getWordReport: async (creditProgramIds: string[]) => {
const idsParam = creditProgramIds.reduce((prev, curr, index) => {
return (prev += `${index === 0 ? '' : '&'}creditProgramIds=${curr}`);
}, '');
console.log('idsParam', idsParam);
const res = await fetch(
`${API_URL}/api/Report/LoadDepositByCreditProgram?creditProgramIds=${idsParam}`,
{
credentials: 'include',
headers: {
'Content-Type': 'application/octet-stream',
},
},
);
if (!res.ok) {
throw new Error(`Не удалось загрузить Word отчет: ${res.statusText}`);
}
return res.blob();
},
sendWordReportByEmail: async (
mailInfo: CreditProgramReportMailSendInfoBindingModel,
) => {
const res = await fetch(
`${API_URL}/api/Report/SendReportDepositByCreditProgram`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(mailInfo),
},
);
if (!res.ok) {
throw new Error(`Не удалось отправить Word отчет: ${res.statusText}`);
}
},
// Excel отчеты
getExcelReport: async (creditProgramIds: string[]) => {
const idsParam = creditProgramIds.reduce((prev, curr, index) => {
return (prev += `${index === 0 ? '' : '&'}creditProgramIds=${curr}`);
}, '');
const res = await fetch(
`${API_URL}/api/Report/LoadExcelDepositByCreditProgram?${idsParam}`,
{
credentials: 'include',
headers: {
'Content-Type': 'application/octet-stream',
},
},
);
if (!res.ok) {
throw new Error(`Не удалось загрузить Excel отчет: ${res.statusText}`);
}
return res.blob();
},
sendExcelReportByEmail: async (
mailInfo: CreditProgramReportMailSendInfoBindingModel,
) => {
const res = await fetch(
`${API_URL}/api/Report/SendExcelReportDepositByCreditProgram`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(mailInfo),
},
);
if (!res.ok) {
throw new Error(`Не удалось отправить Excel отчет: ${res.statusText}`);
}
},
// Получение данных для предпросмотра
getReportData: async (creditProgramIds: string[]) => {
const idsParam = creditProgramIds.join(',');
const res = await fetch(
`${API_URL}/api/Report/GetDepositByCreditProgram?creditProgramIds=${idsParam}`,
{
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
},
);
if (!res.ok) {
throw new Error(`Не удалось загрузить данные отчета: ${res.statusText}`);
}
return res.json();
},
getReportDataByCurrency: async (fromDate: string, toDate: string) => {
const res = await fetch(
`${API_URL}/api/Report/GetDepositAndCreditProgramByCurrency?fromDate=${fromDate}&toDate=${toDate}`,
{
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
},
);
if (!res.ok) {
throw new Error(
`Не удалось загрузить данные отчета по валюте: ${res.statusText}`,
);
}
return res.json();
},
};

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,115 @@
import React from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import 'react-pdf/dist/esm/Page/TextLayer.css';
import { Button } from '../ui/button';
// Настройка worker для PDF.js
pdfjs.GlobalWorkerOptions.workerSrc = `https://cdn.jsdelivr.net/npm/pdfjs-dist@${pdfjs.version}/build/pdf.worker.mjs`;
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 | null>(null);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
if (report?.blob) {
const url = URL.createObjectURL(report.blob);
setPdfUrl(url);
setError(null);
return () => {
URL.revokeObjectURL(url);
};
} else {
setPdfUrl(null);
setNumPages(null);
setPageNumber(1);
}
}, [report]);
const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
setNumPages(numPages);
setPageNumber(1);
setError(null);
};
const onDocumentLoadError = (error: Error) => {
console.error('Ошибка загрузки PDF:', error);
setError(
'Ошибка при загрузке PDF документа. Пожалуйста, попробуйте снова.',
);
};
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 text-center">
{report
? 'Подготовка PDF для отображения...'
: 'Нет данных для отображения PDF.'}
</div>
);
}
return (
<div className="p-4">
<Document
file={pdfUrl}
onLoadSuccess={onDocumentLoadSuccess}
onLoadError={onDocumentLoadError}
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 text-center">{error}</div>
) : numPages ? (
<div className="flex justify-between items-center mt-4">
<Button onClick={handlePrevPage} disabled={pageNumber <= 1}>
Предыдущая
</Button>
<p className="text-sm text-muted-foreground">
Страница {pageNumber} из {numPages}
</p>
<Button onClick={handleNextPage} disabled={pageNumber >= numPages}>
Следующая
</Button>
</div>
) : (
<div className="text-center py-2 text-muted-foreground">
Загрузка документа...
</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,342 @@
import React from 'react';
import { PdfViewer } from './PdfViewer';
import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import { format } from 'date-fns';
import { CalendarIcon, FileText, FileSpreadsheet } from 'lucide-react';
import { ru } from 'date-fns/locale';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Label } from '@/components/ui/label';
import { useCreditPrograms } from '@/hooks/useCreditPrograms';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
type ReportCategory = 'pdf' | 'word-excel' | null;
type FileFormat = 'doc' | 'xls';
interface ReportViewerProps {
selectedCategory: ReportCategory;
onGeneratePdf: (fromDate: Date, toDate: Date) => void;
onDownloadPdf: (fromDate: Date, toDate: Date) => void;
onSendPdfEmail: (fromDate: Date, toDate: Date) => void;
onDownloadWordExcel: (format: FileFormat, creditProgramIds: string[]) => void;
onSendWordExcelEmail: (
format: FileFormat,
creditProgramIds: string[],
) => void;
pdfReport?: { blob: Blob; fileName: string; mimeType: string } | null;
}
export const ReportViewer = ({
selectedCategory,
onGeneratePdf,
onDownloadPdf,
onSendPdfEmail,
onDownloadWordExcel,
onSendWordExcelEmail,
pdfReport,
}: ReportViewerProps): React.JSX.Element => {
const { creditPrograms } = useCreditPrograms();
const [fromDate, setFromDate] = React.useState<Date>();
const [toDate, setToDate] = React.useState<Date>();
const [fileFormat, setFileFormat] = React.useState<FileFormat>('doc');
const [selectedCreditProgramIds, setSelectedCreditProgramIds] =
React.useState<string[]>([]);
const isPdfDatesValid = fromDate && toDate;
const isWordExcelDataValid = selectedCreditProgramIds.length > 0;
const handleCreditProgramSelect = (creditProgramId: string) => {
setSelectedCreditProgramIds((prev) => {
if (prev.includes(creditProgramId)) {
return prev.filter((id) => id !== creditProgramId);
} else {
return [...prev, creditProgramId];
}
});
};
const removeCreditProgram = (creditProgramId: string) => {
setSelectedCreditProgramIds((prev) =>
prev.filter((id) => id !== creditProgramId),
);
};
if (!selectedCategory) {
return (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-semibold mb-2">Выберите тип отчета</h2>
<p className="text-muted-foreground">
Выберите категорию отчета в боковой панели для начала работы
</p>
</div>
</div>
);
}
if (selectedCategory === 'pdf') {
return (
<div className="flex-1 flex flex-col">
{/* Панель управления PDF */}
<div className="border-b p-4 bg-background">
<h2 className="text-xl font-semibold mb-4">
PDF Отчет по валютам и периодам
</h2>
{/* Выбор дат */}
<div className="flex gap-4 mb-4">
<div className="flex-1">
<Label>Дата начала</Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
'w-full pl-3 text-left font-normal',
!fromDate && 'text-muted-foreground',
)}
>
{fromDate ? (
format(fromDate, 'PPP', { locale: ru })
) : (
<span>Выберите дату</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={fromDate}
onSelect={setFromDate}
locale={ru}
initialFocus
/>
</PopoverContent>
</Popover>
</div>
<div className="flex-1">
<Label>Дата окончания</Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
'w-full pl-3 text-left font-normal',
!toDate && 'text-muted-foreground',
)}
>
{toDate ? (
format(toDate, 'PPP', { locale: ru })
) : (
<span>Выберите дату</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={toDate}
onSelect={setToDate}
locale={ru}
initialFocus
/>
</PopoverContent>
</Popover>
</div>
</div>
{/* Кнопки действий */}
<div className="flex gap-2">
<Button
onClick={() =>
isPdfDatesValid && onGeneratePdf(fromDate!, toDate!)
}
disabled={!isPdfDatesValid}
>
Сгенерировать на странице
</Button>
<Button
onClick={() =>
isPdfDatesValid && onDownloadPdf(fromDate!, toDate!)
}
disabled={!isPdfDatesValid}
variant="outline"
>
Скачать
</Button>
<Button
onClick={() =>
isPdfDatesValid && onSendPdfEmail(fromDate!, toDate!)
}
disabled={!isPdfDatesValid}
variant="outline"
>
Отправить на почту
</Button>
</div>
</div>
{/* Область просмотра PDF */}
<div className="flex-1 overflow-auto">
{pdfReport ? (
<PdfViewer report={pdfReport} />
) : (
<div className="flex items-center justify-center h-full">
<p className="text-muted-foreground">
Выберите даты и нажмите "Сгенерировать на странице" для
просмотра отчета
</p>
</div>
)}
</div>
</div>
);
}
if (selectedCategory === 'word-excel') {
return (
<div className="flex-1 p-6">
<h2 className="text-xl font-semibold mb-6">
Word/Excel Отчет по кредитным программам
</h2>
{/* Выбор формата файла */}
<div className="mb-6">
<Label className="text-base font-medium mb-3 block">
Формат файла
</Label>
<RadioGroup
value={fileFormat}
onValueChange={(value) => setFileFormat(value as FileFormat)}
className="flex gap-6"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="doc" id="doc" />
<Label
htmlFor="doc"
className="flex items-center gap-2 cursor-pointer"
>
<FileText className="h-4 w-4" />
Microsoft Word
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="xls" id="xls" />
<Label
htmlFor="xls"
className="flex items-center gap-2 cursor-pointer"
>
<FileSpreadsheet className="h-4 w-4" />
Microsoft Excel
</Label>
</div>
</RadioGroup>
</div>
{/* Выбор кредитных программ */}
<div className="mb-6">
<Label className="text-base font-medium mb-3 block">
Кредитные программы
</Label>
<Select
onValueChange={(value) => {
if (!selectedCreditProgramIds.includes(value)) {
handleCreditProgramSelect(value);
}
}}
>
<SelectTrigger>
<SelectValue placeholder="Выберите кредитные программы" />
</SelectTrigger>
<SelectContent>
{creditPrograms?.map((program) => (
<SelectItem
key={program.id}
value={program.id || ''}
className={cn(
selectedCreditProgramIds.includes(program.id || '') &&
'bg-muted',
)}
>
{program.name}
</SelectItem>
))}
</SelectContent>
</Select>
{/* Отображение выбранных программ */}
<div className="flex flex-wrap gap-2 mt-3">
{selectedCreditProgramIds.map((programId) => {
const program = creditPrograms?.find((p) => p.id === programId);
return (
<div
key={programId}
className="bg-muted px-3 py-1 rounded-md flex items-center gap-2"
>
<span>{program?.name || programId}</span>
<Button
type="button"
variant="ghost"
size="icon"
className="h-4 w-4 rounded-full"
onClick={() => removeCreditProgram(programId)}
>
×
</Button>
</div>
);
})}
</div>
</div>
{/* Кнопки действий */}
<div className="flex gap-2">
<Button
onClick={() =>
isWordExcelDataValid &&
onDownloadWordExcel(fileFormat, selectedCreditProgramIds)
}
disabled={!isWordExcelDataValid}
>
Скачать
</Button>
<Button
onClick={() =>
isWordExcelDataValid &&
onSendWordExcelEmail(fileFormat, selectedCreditProgramIds)
}
disabled={!isWordExcelDataValid}
variant="outline"
>
Отправить на почту
</Button>
</div>
{!isWordExcelDataValid && (
<p className="text-sm text-muted-foreground mt-4">
Выберите хотя бы одну кредитную программу для активации кнопок
</p>
)}
</div>
);
}
return <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,64 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Label } from '@/components/ui/label';
import { FileText, FileSpreadsheet } from 'lucide-react';
type ReportCategory = 'pdf' | 'word-excel' | null;
interface ReportSidebarProps {
selectedCategory: ReportCategory;
onCategoryChange: (category: ReportCategory) => void;
}
export const ReportSidebar = ({
selectedCategory,
onCategoryChange,
}: ReportSidebarProps): React.JSX.Element => {
return (
<div className="w-80 border-r bg-background">
<div className="space-y-4 p-4">
<div>
<h3 className="mb-4 text-lg font-medium">Категории отчетов</h3>
<RadioGroup
value={selectedCategory || ''}
onValueChange={(value) => onCategoryChange(value as ReportCategory)}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="pdf" id="pdf" />
<Label
htmlFor="pdf"
className="flex items-center gap-2 cursor-pointer"
>
<FileText className="h-4 w-4" />
PDF отчет по валютам и периодам
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="word-excel" id="word-excel" />
<Label
htmlFor="word-excel"
className="flex items-center gap-2 cursor-pointer"
>
<FileSpreadsheet className="h-4 w-4" />
Word/Excel отчет по кредитным программам
</Label>
</div>
</RadioGroup>
</div>
{selectedCategory && (
<div className="pt-4 border-t">
<Button
variant="outline"
onClick={() => onCategoryChange(null)}
className="w-full"
>
Сбросить выбор
</Button>
</div>
)}
</div>
</div>
);
};

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>
);
};

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