Merge pull request 'Task_7.5_UI_Reports' (#12) from Task_7.5_UI_Reports into main
Reviewed-on: #12
This commit was merged in pull request #12.
This commit is contained in:
@@ -33,13 +33,16 @@ public class ReportContract(IClientStorageContract clientStorage, ICurrencyStora
|
||||
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);
|
||||
|
||||
var filteredPrograms = creditPrograms
|
||||
.Where(cp => cp.Currencies.Any()) // Проверяем, что у кредитной программы есть связанные валюты
|
||||
.Where(cp => creditProgramIds == null || creditProgramIds.Contains(cp.Id));
|
||||
.Where(cp => creditProgramIds.Contains(cp.Id));
|
||||
|
||||
return filteredPrograms
|
||||
.Select(cp => new ClientsByCreditProgramDataModel
|
||||
@@ -76,20 +79,20 @@ public class ReportContract(IClientStorageContract clientStorage, ICurrencyStora
|
||||
{
|
||||
for (int i = 0; i < program.ClientSurname.Count; i++)
|
||||
{
|
||||
tableRows.Add(new string[]
|
||||
{
|
||||
tableRows.Add(
|
||||
[
|
||||
program.CreditProgramName,
|
||||
program.ClientSurname[i],
|
||||
program.ClientName[i],
|
||||
program.ClientBalance[i].ToString("N2")
|
||||
});
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return _baseWordBuilder
|
||||
.AddHeader("Клиенты по кредитным программам")
|
||||
.AddParagraph($"Сформировано на дату {DateTime.Now}")
|
||||
.AddTable([100, 100, 100, 100], tableRows)
|
||||
.AddTable([2000, 2000, 2000, 2000], tableRows)
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -180,10 +183,10 @@ 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;
|
||||
}
|
||||
|
||||
@@ -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 параметры
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -88,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)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using BankBusinessLogic.Implementations;
|
||||
using BankContracts.AdapterContracts;
|
||||
using BankContracts.BindingModels;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -8,16 +9,11 @@ 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;
|
||||
private readonly IReportAdapter _adapter = adapter;
|
||||
private readonly EmailService _emailService = EmailService.CreateYandexService();
|
||||
|
||||
public ReportController(IReportAdapter adapter)
|
||||
{
|
||||
_adapter = adapter;
|
||||
_emailService = EmailService.CreateYandexService();
|
||||
}
|
||||
/// <summary>
|
||||
/// Получение данных Клиента по Кредитным программам
|
||||
/// </summary>
|
||||
@@ -81,8 +77,7 @@ public class ReportController : ControllerBase
|
||||
[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>
|
||||
@@ -126,6 +121,7 @@ public class ReportController : ControllerBase
|
||||
|
||||
/// <summary>
|
||||
/// Получение данных Вклада и Кредитных программам по Валютам
|
||||
/// кладовщик pdf
|
||||
/// </summary>
|
||||
/// <param name="fromDate"></param>
|
||||
/// <param name="toDate"></param>
|
||||
@@ -135,12 +131,12 @@ public class ReportController : ControllerBase
|
||||
[Consumes("application/json")]
|
||||
public async Task<IActionResult> GetDepositAndCreditProgramByCurrency(DateTime fromDate, DateTime toDate, CancellationToken cancellationToken)
|
||||
{
|
||||
return (await _adapter.GetDataDepositAndCreditProgramByCurrencyAsync(fromDate, toDate,
|
||||
cancellationToken)).GetResponse(Request, Response);
|
||||
return (await _adapter.GetDataDepositAndCreditProgramByCurrencyAsync(fromDate, toDate, cancellationToken)).GetResponse(Request, Response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отчет pdf Вклада и Кредитных программам по Валютам
|
||||
/// кладовщик pdf
|
||||
/// </summary>
|
||||
/// <param name="fromDate"></param>
|
||||
/// <param name="toDate"></param>
|
||||
@@ -156,34 +152,36 @@ public class ReportController : ControllerBase
|
||||
/// <summary>
|
||||
/// Отправка word отчета Клиентов по Кредитным программам
|
||||
/// </summary>
|
||||
/// <param name="email"></param>
|
||||
/// <param name="creditProgramIds"></param>
|
||||
/// <param name="mailInfo"></param>
|
||||
/// <param name="ct"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> SendReportByCreditProgram(string email, [FromQuery] List<string>? creditProgramIds, CancellationToken ct)
|
||||
public async Task<IActionResult> SendReportByCreditProgram([FromBody] CreditProgramReportMailSendInfoBindingModel mailInfo, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var report = await _adapter.CreateDocumentClientsByCreditProgramAsync(creditProgramIds, 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("Отчет успешно отправлен на почту");
|
||||
}
|
||||
|
||||
@@ -196,37 +194,40 @@ public class ReportController : ControllerBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отправка pdf отчета Клиентов по Валютам
|
||||
/// Отправка pdf отчета Клиентов по Депозитам
|
||||
/// </summary>
|
||||
/// <param name="email"></param>
|
||||
/// <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("Отчет успешно отправлен на почту");
|
||||
}
|
||||
|
||||
@@ -240,36 +241,40 @@ public class ReportController : ControllerBase
|
||||
|
||||
/// <summary>
|
||||
/// Отправка pdf отчета Вкладов и Кредитных программ по Валютам
|
||||
/// кладовщик pdf
|
||||
/// </summary>
|
||||
/// <param name="email"></param>
|
||||
/// <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("Отчет успешно отправлен на почту");
|
||||
}
|
||||
|
||||
@@ -282,36 +287,38 @@ public class ReportController : ControllerBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отправка excel отчета Клиентов по Кредитных программ
|
||||
/// Отправка excel отчета Клиентов по Кредитным программам
|
||||
/// </summary>
|
||||
/// <param name="email"></param>
|
||||
/// <param name="creditProgramIds"></param>
|
||||
/// <param name="mailInfo"></param>
|
||||
/// <param name="ct"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> SendExcelReportByCreditProgram(string email, [FromQuery] List<string>? creditProgramIds, CancellationToken ct)
|
||||
public async Task<IActionResult> SendExcelReportByCreditProgram([FromBody] CreditProgramReportMailSendInfoBindingModel mailInfo, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var report = await _adapter.CreateExcelDocumentClientsByCreditProgramAsync(creditProgramIds, 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 отчет успешно отправлен на почту");
|
||||
}
|
||||
|
||||
@@ -326,34 +333,36 @@ public class ReportController : ControllerBase
|
||||
/// <summary>
|
||||
/// Отправка word отчета Вкладов по Кредитных программ
|
||||
/// </summary>
|
||||
/// <param name="email"></param>
|
||||
/// <param name="creditProgramIds"></param>
|
||||
/// <param name="mailInfo"></param>
|
||||
/// <param name="ct"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> SendReportDepositByCreditProgram(string email, [FromQuery] List<string>? creditProgramIds, CancellationToken ct)
|
||||
public async Task<IActionResult> SendReportDepositByCreditProgram([FromBody] CreditProgramReportMailSendInfoBindingModel mailInfo, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var report = await _adapter.CreateDocumentDepositByCreditProgramAsync(creditProgramIds, 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("Отчет успешно отправлен на почту");
|
||||
}
|
||||
|
||||
@@ -368,34 +377,36 @@ public class ReportController : ControllerBase
|
||||
/// <summary>
|
||||
/// Отправка excel отчета Вкладов по Кредитных программ
|
||||
/// </summary>
|
||||
/// <param name="email"></param>
|
||||
/// <param name="creditProgramIds"></param>
|
||||
/// <param name="mailInfo"></param>
|
||||
/// <param name="ct"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> SendExcelReportDepositByCreditProgram(string email, [FromQuery] List<string>? creditProgramIds, CancellationToken ct)
|
||||
public async Task<IActionResult> SendExcelReportDepositByCreditProgram([FromBody] CreditProgramReportMailSendInfoBindingModel mailInfo, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var report = await _adapter.CreateExcelDocumentDepositByCreditProgramAsync(creditProgramIds, 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 отчет успешно отправлен на почту");
|
||||
}
|
||||
|
||||
|
||||
74
TheBank/BankWebApi/Controllers/clerkReports.http
Normal file
74
TheBank/BankWebApi/Controllers/clerkReports.http
Normal 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.
|
||||
76
TheBank/BankWebApi/Controllers/storekeeperReports.http
Normal file
76
TheBank/BankWebApi/Controllers/storekeeperReports.http
Normal 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.
|
||||
```
|
||||
2
TheBank/bankui/.gitignore
vendored
2
TheBank/bankui/.gitignore
vendored
@@ -22,3 +22,5 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
package-lock.json
|
||||
|
||||
Binary file not shown.
4662
TheBank/bankui/package-lock.json
generated
4662
TheBank/bankui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,20 +13,22 @@
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@radix-ui/react-avatar": "^1.1.9",
|
||||
"@radix-ui/react-checkbox": "^1.3.1",
|
||||
"@radix-ui/react-dialog": "^1.1.13",
|
||||
"@radix-ui/react-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.2",
|
||||
"@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",
|
||||
|
||||
File diff suppressed because one or more lines are too long
162
TheBank/bankui/src/api/reports.ts
Normal file
162
TheBank/bankui/src/api/reports.ts
Normal 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();
|
||||
},
|
||||
};
|
||||
@@ -1,12 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Document, Page } from 'react-pdf';
|
||||
import * as pdfjs from 'pdfjs-dist';
|
||||
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
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`;
|
||||
// Настройка 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;
|
||||
@@ -15,25 +14,36 @@ interface PdfViewerProps {
|
||||
export const PdfViewer = ({ report }: PdfViewerProps) => {
|
||||
const [numPages, setNumPages] = React.useState<number | null>(null);
|
||||
const [pageNumber, setPageNumber] = React.useState(1);
|
||||
const [pdfUrl, setPdfUrl] = React.useState<string | undefined>(undefined);
|
||||
const [pdfUrl, setPdfUrl] = React.useState<string | null>(null);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
// Создаем URL для Blob при изменении report
|
||||
React.useEffect(() => {
|
||||
if (report?.blob) {
|
||||
const url = URL.createObjectURL(report.blob);
|
||||
setPdfUrl(url);
|
||||
setError(null);
|
||||
// Очищаем URL при размонтировании компонента или изменении report
|
||||
return () => URL.revokeObjectURL(url);
|
||||
|
||||
return () => {
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
} else {
|
||||
setPdfUrl(undefined);
|
||||
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 = () => {
|
||||
@@ -46,7 +56,11 @@ export const PdfViewer = ({ report }: PdfViewerProps) => {
|
||||
|
||||
if (!pdfUrl) {
|
||||
return (
|
||||
<div className="p-4">Загрузка или нет данных для отображения PDF.</div>
|
||||
<div className="p-4 text-center">
|
||||
{report
|
||||
? 'Подготовка PDF для отображения...'
|
||||
: 'Нет данных для отображения PDF.'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,12 +69,7 @@ export const PdfViewer = ({ report }: PdfViewerProps) => {
|
||||
<Document
|
||||
file={pdfUrl}
|
||||
onLoadSuccess={onDocumentLoadSuccess}
|
||||
onLoadError={(error) => {
|
||||
console.error('Ошибка загрузки PDF:', error);
|
||||
setError(
|
||||
'Ошибка при загрузке PDF документа. Пожалуйста, попробуйте снова.',
|
||||
);
|
||||
}}
|
||||
onLoadError={onDocumentLoadError}
|
||||
loading={<div className="text-center py-4">Загрузка PDF...</div>}
|
||||
error={
|
||||
<div className="text-center text-red-500 py-4">
|
||||
@@ -83,22 +92,23 @@ export const PdfViewer = ({ report }: PdfViewerProps) => {
|
||||
</Document>
|
||||
|
||||
{error ? (
|
||||
<div className="text-red-500 py-2">{error}</div>
|
||||
) : (
|
||||
<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>
|
||||
Страница {pageNumber} из {numPages || 1}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Страница {pageNumber} из {numPages}
|
||||
</p>
|
||||
<Button
|
||||
onClick={handleNextPage}
|
||||
disabled={pageNumber >= (numPages || 1)}
|
||||
>
|
||||
<Button onClick={handleNextPage} disabled={pageNumber >= numPages}>
|
||||
Следующая
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-2 text-muted-foreground">
|
||||
Загрузка документа...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,518 +1,342 @@
|
||||
import React from 'react';
|
||||
import type { SelectedReport } from '../pages/Reports';
|
||||
import { Button } from '../ui/button';
|
||||
import { PdfViewer } from './PdfViewer';
|
||||
import { DialogForm } from '../layout/DialogForm';
|
||||
import { Input } from '../ui/input';
|
||||
import { Label } from '../ui/label';
|
||||
import { toast } from 'sonner';
|
||||
import { Calendar } from '../ui/calendar';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
||||
import { 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 } from 'lucide-react';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
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 {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '../ui/form';
|
||||
import { ConfigManager } from '@/lib/config';
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
|
||||
type ReportViewerProps = {
|
||||
selectedReport: SelectedReport;
|
||||
};
|
||||
type ReportCategory = 'pdf' | 'word-excel' | null;
|
||||
type FileFormat = 'doc' | 'xls';
|
||||
|
||||
type ReportData = { blob: Blob; fileName: string; mimeType: string };
|
||||
|
||||
const emailFormSchema = z.object({
|
||||
toEmail: z.string().email({ message: 'Введите корректный email' }),
|
||||
subject: z.string().min(1, { message: 'Тема обязательна' }),
|
||||
body: z.string().min(1, { message: 'Текст сообщения обязателен' }),
|
||||
});
|
||||
|
||||
const API_URL = ConfigManager.loadUrl();
|
||||
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 = ({
|
||||
selectedReport,
|
||||
selectedCategory,
|
||||
onGeneratePdf,
|
||||
onDownloadPdf,
|
||||
onSendPdfEmail,
|
||||
onDownloadWordExcel,
|
||||
onSendWordExcelEmail,
|
||||
pdfReport,
|
||||
}: ReportViewerProps): React.JSX.Element => {
|
||||
const [isSendDialogOpen, setIsSendDialogOpen] = React.useState(false);
|
||||
const [fromDate, setFromDate] = React.useState<Date | undefined>(undefined);
|
||||
const [toDate, setToDate] = React.useState<Date | undefined>(undefined);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState<Error | null>(null);
|
||||
const [report, setReport] = React.useState<ReportData | null>(null);
|
||||
const { creditPrograms } = useCreditPrograms();
|
||||
|
||||
const form = useForm<z.infer<typeof emailFormSchema>>({
|
||||
resolver: zodResolver(emailFormSchema),
|
||||
defaultValues: {
|
||||
toEmail: '',
|
||||
subject: getDefaultSubject(selectedReport),
|
||||
body: getDefaultBody(selectedReport, fromDate, toDate),
|
||||
},
|
||||
});
|
||||
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[]>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
form.setValue('subject', getDefaultSubject(selectedReport));
|
||||
form.setValue('body', getDefaultBody(selectedReport, fromDate, toDate));
|
||||
const isPdfDatesValid = fromDate && toDate;
|
||||
const isWordExcelDataValid = selectedCreditProgramIds.length > 0;
|
||||
|
||||
setReport(null);
|
||||
}, [selectedReport, fromDate, toDate, form]);
|
||||
|
||||
const getReportTitle = (report: SelectedReport) => {
|
||||
switch (report) {
|
||||
case 'word':
|
||||
return 'Отчет Word по вкладам по кредитным программам';
|
||||
case 'excel':
|
||||
return 'Отчет Excel по вкладам по кредитным программам';
|
||||
case 'pdf':
|
||||
return 'Отчет PDF по вкладам и кредитным программам по валютам';
|
||||
default:
|
||||
return 'Выберите тип отчета';
|
||||
}
|
||||
const handleCreditProgramSelect = (creditProgramId: string) => {
|
||||
setSelectedCreditProgramIds((prev) => {
|
||||
if (prev.includes(creditProgramId)) {
|
||||
return prev.filter((id) => id !== creditProgramId);
|
||||
} else {
|
||||
return [...prev, creditProgramId];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function getDefaultSubject(report: SelectedReport | undefined): string {
|
||||
switch (report) {
|
||||
case 'word':
|
||||
return 'Отчет по вкладам по кредитным программам';
|
||||
case 'excel':
|
||||
return 'Excel отчет по вкладам по кредитным программам';
|
||||
case 'pdf':
|
||||
return 'Отчет по вкладам и кредитным программам по валютам';
|
||||
default:
|
||||
return 'Отчет';
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultBody(
|
||||
report: SelectedReport | undefined,
|
||||
fromDate?: Date,
|
||||
toDate?: Date,
|
||||
): string {
|
||||
switch (report) {
|
||||
case 'word':
|
||||
return 'В приложении находится отчет по вкладам по кредитным программам.';
|
||||
case 'excel':
|
||||
return 'В приложении находится Excel отчет по вкладам по кредитным программам.';
|
||||
case 'pdf':
|
||||
return `В приложении находится отчет по вкладам и кредитным программам по валютам${
|
||||
fromDate && toDate
|
||||
? ` за период с ${format(fromDate, 'dd.MM.yyyy')} по ${format(
|
||||
toDate,
|
||||
'dd.MM.yyyy',
|
||||
)}`
|
||||
: ''
|
||||
}.`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const getReportUrl = (
|
||||
selectedReport: SelectedReport,
|
||||
fromDate?: Date,
|
||||
toDate?: Date,
|
||||
): string => {
|
||||
switch (selectedReport) {
|
||||
case 'word':
|
||||
return `${API_URL}/api/Report/LoadDepositByCreditProgram`;
|
||||
case 'excel':
|
||||
return `${API_URL}/api/Report/LoadExcelDepositByCreditProgram`;
|
||||
case 'pdf': {
|
||||
if (!fromDate || !toDate) {
|
||||
throw new Error('Необходимо выбрать даты для PDF отчета');
|
||||
}
|
||||
const fromDateStr = format(fromDate, 'yyyy-MM-dd');
|
||||
const toDateStr = format(toDate, 'yyyy-MM-dd');
|
||||
return `${API_URL}/api/Report/LoadDepositAndCreditProgramByCurrency?fromDate=${fromDateStr}&toDate=${toDateStr}`;
|
||||
}
|
||||
default:
|
||||
throw new Error('Выберите тип отчета');
|
||||
}
|
||||
};
|
||||
|
||||
const getSendEmailUrl = (selectedReport: SelectedReport): string => {
|
||||
switch (selectedReport) {
|
||||
case 'word':
|
||||
return `${API_URL}/api/Report/SendReportDepositByCreditProgram`;
|
||||
case 'excel':
|
||||
return `${API_URL}/api/Report/SendExcelReportDepositByCreditProgram`;
|
||||
case 'pdf': {
|
||||
if (!fromDate || !toDate) {
|
||||
throw new Error('Необходимо выбрать даты для PDF отчета');
|
||||
}
|
||||
const fromDateStr = format(fromDate, 'yyyy-MM-dd');
|
||||
const toDateStr = format(toDate, 'yyyy-MM-dd');
|
||||
return `${API_URL}/api/Report/SendReportByCurrency?fromDate=${fromDateStr}&toDate=${toDateStr}`;
|
||||
}
|
||||
default:
|
||||
throw new Error('Выберите тип отчета');
|
||||
}
|
||||
};
|
||||
|
||||
const fetchReport = async (): Promise<ReportData> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const url = getReportUrl(selectedReport, fromDate, toDate);
|
||||
console.log(`Загружаем отчет с URL: ${url}`);
|
||||
|
||||
const acceptHeader =
|
||||
selectedReport === 'pdf'
|
||||
? 'application/pdf'
|
||||
: selectedReport === 'word'
|
||||
? 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: acceptHeader,
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Ошибка загрузки отчета: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
console.log(
|
||||
`Отчет загружен. Тип: ${blob.type}, размер: ${blob.size} байт`,
|
||||
);
|
||||
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
const defaultExtension =
|
||||
selectedReport === 'pdf'
|
||||
? '.pdf'
|
||||
: selectedReport === 'word'
|
||||
? '.docx'
|
||||
: '.xlsx';
|
||||
let fileName = `report${defaultExtension}`;
|
||||
|
||||
if (contentDisposition && contentDisposition.includes('filename=')) {
|
||||
fileName = contentDisposition
|
||||
.split('filename=')[1]
|
||||
.replace(/"/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
const mimeType = response.headers.get('Content-Type') || '';
|
||||
|
||||
const reportData = { blob, fileName, mimeType };
|
||||
setReport(reportData);
|
||||
return reportData;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке отчета:', error);
|
||||
const err =
|
||||
error instanceof Error ? error : new Error('Неизвестная ошибка');
|
||||
setError(err);
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
try {
|
||||
await fetchReport();
|
||||
toast.success('Отчет успешно загружен');
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
`Ошибка загрузки отчета: ${
|
||||
error instanceof Error ? error.message : 'Неизвестная ошибка'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
let reportData = report;
|
||||
// Для PDF всегда делаем новый запрос с актуальными датами
|
||||
if (selectedReport === 'pdf') {
|
||||
if (!fromDate || !toDate) {
|
||||
toast.error('Пожалуйста, выберите даты для PDF отчета');
|
||||
return;
|
||||
}
|
||||
toast.loading('Загрузка отчета...');
|
||||
reportData = await fetchReport();
|
||||
} else if (!reportData) {
|
||||
toast.loading('Загрузка отчета...');
|
||||
reportData = await fetchReport();
|
||||
}
|
||||
|
||||
// Скачиваем отчет
|
||||
const url = URL.createObjectURL(reportData.blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = reportData.fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
toast.success('Отчет успешно скачан');
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
`Ошибка при скачивании отчета: ${
|
||||
error instanceof Error ? error.message : 'Неизвестная ошибка'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendFormSubmit = async (
|
||||
values: z.infer<typeof emailFormSchema>,
|
||||
) => {
|
||||
try {
|
||||
// Если выбран PDF отчет, проверяем наличие дат
|
||||
if (selectedReport === 'pdf' && (!fromDate || !toDate)) {
|
||||
toast.error('Пожалуйста, выберите даты для PDF отчета');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
// Формируем данные для отправки
|
||||
const url = getSendEmailUrl(selectedReport);
|
||||
|
||||
// Параметры для запроса
|
||||
const data: Record<string, string> = {
|
||||
toEmail: values.toEmail,
|
||||
subject: values.subject,
|
||||
body: values.body,
|
||||
};
|
||||
|
||||
// Отправляем запрос
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(
|
||||
`Ошибка при отправке отчета: ${response.status} ${response.statusText}\n${errorText}`,
|
||||
);
|
||||
}
|
||||
|
||||
toast.success('Отчет успешно отправлен на почту');
|
||||
setIsSendDialogOpen(false);
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
console.error('Ошибка при отправке отчета:', error);
|
||||
toast.error(
|
||||
`Ошибка при отправке отчета: ${
|
||||
error instanceof Error ? error.message : 'Неизвестная ошибка'
|
||||
}`,
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Проверка, можно ли сгенерировать/скачать/отправить PDF отчет
|
||||
const isPdfActionDisabled =
|
||||
selectedReport === 'pdf' && (!fromDate || !toDate || isLoading);
|
||||
|
||||
// Отображение ошибки, если она есть
|
||||
const renderError = () => {
|
||||
if (!error) return null;
|
||||
|
||||
return (
|
||||
<div className="p-4 border border-red-300 bg-red-50 rounded-md mt-2">
|
||||
<h3 className="text-red-700 font-semibold mb-1">Детали ошибки:</h3>
|
||||
<p className="text-red-600 whitespace-pre-wrap break-words">
|
||||
{error.message}
|
||||
</p>
|
||||
</div>
|
||||
const removeCreditProgram = (creditProgramId: string) => {
|
||||
setSelectedCreditProgramIds((prev) =>
|
||||
prev.filter((id) => id !== creditProgramId),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="text-lg font-semibold mb-4">
|
||||
{getReportTitle(selectedReport)}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
{/* Кнопки действий */}
|
||||
<div className="flex gap-4 mb-4">
|
||||
{/* Кнопка "Сгенерировать" только для PDF с выбранными датами */}
|
||||
{selectedReport === 'pdf' && (
|
||||
<Button onClick={handleGenerate} disabled={isPdfActionDisabled}>
|
||||
{isLoading ? 'Загрузка...' : 'Сгенерировать'}
|
||||
</Button>
|
||||
)}
|
||||
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>
|
||||
|
||||
{/* Кнопки "Скачать" и "Отправить" только когда выбран тип отчета */}
|
||||
{selectedReport && (
|
||||
<>
|
||||
<Button
|
||||
onClick={handleDownload}
|
||||
disabled={isPdfActionDisabled || isLoading}
|
||||
>
|
||||
{isLoading ? 'Загрузка...' : 'Скачать'}
|
||||
</Button>
|
||||
{/* Выбор дат */}
|
||||
<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>
|
||||
|
||||
<Button
|
||||
onClick={() => setIsSendDialogOpen(true)}
|
||||
disabled={isPdfActionDisabled || isLoading}
|
||||
>
|
||||
Отправить
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Календари для выбора периода для PDF отчета */}
|
||||
{selectedReport === 'pdf' && (
|
||||
<div className="flex gap-4 mb-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="fromDate">От даты</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
className={cn(
|
||||
'w-[240px] justify-start text-left font-normal',
|
||||
!fromDate && 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{fromDate ? (
|
||||
format(fromDate, 'PPP')
|
||||
) : (
|
||||
<span>Выберите дату</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={fromDate}
|
||||
onSelect={setFromDate}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div 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="grid gap-2">
|
||||
<Label htmlFor="toDate">До даты</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
className={cn(
|
||||
'w-[240px] justify-start text-left font-normal',
|
||||
!toDate && 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{toDate ? format(toDate, 'PPP') : <span>Выберите дату</span>}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={toDate}
|
||||
onSelect={setToDate}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* Кнопки действий */}
|
||||
<div 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>
|
||||
)}
|
||||
|
||||
{/* Форма отправки отчета на почту */}
|
||||
<DialogForm
|
||||
title="Отправка отчета"
|
||||
description="Введите данные для отправки отчета"
|
||||
isOpen={isSendDialogOpen}
|
||||
onClose={() => setIsSendDialogOpen(false)}
|
||||
onSubmit={form.handleSubmit(handleSendFormSubmit)}
|
||||
>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleSendFormSubmit)}
|
||||
className="space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="toEmail"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email получателя</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="example@mail.ru" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="subject"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Тема письма</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="body"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Текст сообщения</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Отправить</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogForm>
|
||||
|
||||
<div className="mt-4">
|
||||
{isLoading && <div className="p-4">Загрузка документа...</div>}
|
||||
|
||||
{renderError()}
|
||||
|
||||
{!selectedReport && !isLoading && !error && (
|
||||
<div className="p-4">Выберите тип отчета из боковой панели</div>
|
||||
)}
|
||||
|
||||
{selectedReport && !report && !isLoading && !error && (
|
||||
<div className="p-4">
|
||||
{selectedReport === 'pdf'
|
||||
? 'Выберите даты и нажмите "Сгенерировать"'
|
||||
: 'Нажмите "Скачать" для загрузки отчета'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedReport === 'pdf' && report && <PdfViewer report={report} />}
|
||||
{/* Область просмотра 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>
|
||||
</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>;
|
||||
};
|
||||
|
||||
@@ -1,58 +1,64 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
} from '@/components/ui/sidebar';
|
||||
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;
|
||||
}
|
||||
|
||||
type SidebarProps = {
|
||||
onWordClick: () => void;
|
||||
onPdfClick: () => void;
|
||||
onExcelClick: () => void;
|
||||
};
|
||||
export const ReportSidebar = ({
|
||||
onWordClick,
|
||||
onExcelClick,
|
||||
onPdfClick,
|
||||
}: SidebarProps): React.JSX.Element => {
|
||||
selectedCategory,
|
||||
onCategoryChange,
|
||||
}: ReportSidebarProps): React.JSX.Element => {
|
||||
return (
|
||||
<SidebarProvider className="w-[400px]">
|
||||
<Sidebar variant="floating" collapsible="none">
|
||||
<SidebarContent />
|
||||
<SidebarGroupContent className="">
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild onClick={onWordClick}>
|
||||
<span>
|
||||
<img src="/icons/word.svg" alt="word-icon" />
|
||||
отчет word КЛАДОВЩИКА
|
||||
</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild onClick={onExcelClick}>
|
||||
<span>
|
||||
<img src="/icons/excel.svg" alt="excel-icon" />
|
||||
отчет excel КЛАДОВЩИКА
|
||||
</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild onClick={onPdfClick}>
|
||||
<span className="p-5">
|
||||
<img src="/icons/pdf.svg" alt="pdf-icon" />
|
||||
отчет pdf КЛАДОВЩИКА
|
||||
</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</Sidebar>
|
||||
</SidebarProvider>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,20 +1,237 @@
|
||||
import React from 'react';
|
||||
import { ReportSidebar } from '../layout/ReportSidebar';
|
||||
import { ReportViewer } from '../features/ReportViewer';
|
||||
import { reportsApi } from '@/api/reports';
|
||||
import { format } from 'date-fns';
|
||||
import { toast } from 'sonner';
|
||||
import { EmailDialog } from '@/components/ui/EmailDialog';
|
||||
import type {
|
||||
CreditProgramReportMailSendInfoBindingModel,
|
||||
DepositReportMailSendInfoBindingModel,
|
||||
} from '@/types/types';
|
||||
|
||||
export type SelectedReport = 'word' | 'pdf' | 'excel' | undefined;
|
||||
type ReportCategory = 'pdf' | 'word-excel' | null;
|
||||
type FileFormat = 'doc' | 'xls';
|
||||
|
||||
export const Reports = (): React.JSX.Element => {
|
||||
const [selectedReport, setSelectedReport] = React.useState<SelectedReport>();
|
||||
const [selectedCategory, setSelectedCategory] =
|
||||
React.useState<ReportCategory>(null);
|
||||
const [pdfReport, setPdfReport] = React.useState<{
|
||||
blob: Blob;
|
||||
fileName: string;
|
||||
mimeType: string;
|
||||
} | null>(null);
|
||||
const [isEmailDialogOpen, setIsEmailDialogOpen] = React.useState(false);
|
||||
const [isEmailLoading, setIsEmailLoading] = React.useState(false);
|
||||
const [pendingEmailAction, setPendingEmailAction] = React.useState<
|
||||
| {
|
||||
type: 'pdf';
|
||||
data: { fromDate: Date; toDate: Date };
|
||||
}
|
||||
| {
|
||||
type: 'word-excel';
|
||||
data: { format: FileFormat; creditProgramIds: string[] };
|
||||
}
|
||||
| null
|
||||
>(null);
|
||||
|
||||
const handleGeneratePdf = async (fromDate: Date, toDate: Date) => {
|
||||
try {
|
||||
const blob = await reportsApi.getPdfReport(
|
||||
format(fromDate, 'yyyy-MM-dd'),
|
||||
format(toDate, 'yyyy-MM-dd'),
|
||||
);
|
||||
setPdfReport({
|
||||
blob,
|
||||
fileName: `report-${format(new Date(), 'yyyy-MM-dd')}.pdf`,
|
||||
mimeType: 'application/pdf',
|
||||
});
|
||||
toast.success('PDF отчет успешно сгенерирован');
|
||||
} catch (error) {
|
||||
toast.error('Ошибка при генерации PDF отчета');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadPdf = async (fromDate: Date, toDate: Date) => {
|
||||
try {
|
||||
const blob = await reportsApi.getPdfReport(
|
||||
format(fromDate, 'yyyy-MM-dd'),
|
||||
format(toDate, 'yyyy-MM-dd'),
|
||||
);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `pdf-report-${format(new Date(), 'yyyy-MM-dd')}.pdf`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
toast.success('PDF отчет успешно скачан');
|
||||
} catch (error) {
|
||||
toast.error('Ошибка при скачивании PDF отчета');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendPdfEmail = (fromDate: Date, toDate: Date) => {
|
||||
setPendingEmailAction({
|
||||
type: 'pdf',
|
||||
data: { fromDate, toDate },
|
||||
});
|
||||
setIsEmailDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDownloadWordExcel = async (
|
||||
fileFormat: FileFormat,
|
||||
creditProgramIds: string[],
|
||||
) => {
|
||||
try {
|
||||
console.log('cpIds>>', creditProgramIds);
|
||||
const blob =
|
||||
fileFormat === 'doc'
|
||||
? await reportsApi.getWordReport(creditProgramIds)
|
||||
: await reportsApi.getExcelReport(creditProgramIds);
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${fileFormat}-report-${format(new Date(), 'yyyy-MM-dd')}.${
|
||||
fileFormat === 'doc' ? 'docx' : 'xlsx'
|
||||
}`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
toast.success(`${fileFormat.toUpperCase()} отчет успешно скачан`);
|
||||
} catch (error) {
|
||||
toast.error(`Ошибка при скачивании ${fileFormat.toUpperCase()} отчета`);
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendWordExcelEmail = (
|
||||
fileFormat: FileFormat,
|
||||
creditProgramIds: string[],
|
||||
) => {
|
||||
setPendingEmailAction({
|
||||
type: 'word-excel',
|
||||
data: { format: fileFormat, creditProgramIds },
|
||||
});
|
||||
setIsEmailDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleEmailSubmit = async (emailData: {
|
||||
email: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
}) => {
|
||||
if (!pendingEmailAction) {
|
||||
toast.error('Нет данных для отправки');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsEmailLoading(true);
|
||||
try {
|
||||
if (pendingEmailAction.type === 'pdf') {
|
||||
const { fromDate, toDate } = pendingEmailAction.data;
|
||||
const mailInfo: DepositReportMailSendInfoBindingModel = {
|
||||
email: emailData.email,
|
||||
toEmail: emailData.email,
|
||||
subject: emailData.subject,
|
||||
body: emailData.body,
|
||||
fromDate: format(fromDate, 'yyyy-MM-dd'),
|
||||
toDate: format(toDate, 'yyyy-MM-dd'),
|
||||
};
|
||||
await reportsApi.sendPdfReportByEmail(
|
||||
mailInfo,
|
||||
format(fromDate, 'yyyy-MM-dd'),
|
||||
format(toDate, 'yyyy-MM-dd'),
|
||||
);
|
||||
} else {
|
||||
const { format: fileFormat, creditProgramIds } =
|
||||
pendingEmailAction.data;
|
||||
const mailInfo: CreditProgramReportMailSendInfoBindingModel = {
|
||||
email: emailData.email,
|
||||
toEmail: emailData.email,
|
||||
subject: emailData.subject,
|
||||
body: emailData.body,
|
||||
creditProgramIds,
|
||||
};
|
||||
if (fileFormat === 'doc') {
|
||||
await reportsApi.sendWordReportByEmail(mailInfo);
|
||||
} else {
|
||||
await reportsApi.sendExcelReportByEmail(mailInfo);
|
||||
}
|
||||
}
|
||||
|
||||
toast.success('Отчет успешно отправлен на email');
|
||||
setIsEmailDialogOpen(false);
|
||||
setPendingEmailAction(null);
|
||||
} catch (error) {
|
||||
toast.error('Ошибка при отправке отчета на email');
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsEmailLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCategoryChange = (category: ReportCategory) => {
|
||||
setSelectedCategory(category);
|
||||
// Сбрасываем PDF отчет при смене категории
|
||||
if (category !== 'pdf') {
|
||||
setPdfReport(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="flex">
|
||||
<ReportSidebar
|
||||
onWordClick={() => setSelectedReport('word')}
|
||||
onPdfClick={() => setSelectedReport('pdf')}
|
||||
onExcelClick={() => setSelectedReport('excel')}
|
||||
<>
|
||||
<div className="flex h-screen">
|
||||
<ReportSidebar
|
||||
selectedCategory={selectedCategory}
|
||||
onCategoryChange={handleCategoryChange}
|
||||
/>
|
||||
|
||||
<ReportViewer
|
||||
selectedCategory={selectedCategory}
|
||||
onGeneratePdf={handleGeneratePdf}
|
||||
onDownloadPdf={handleDownloadPdf}
|
||||
onSendPdfEmail={handleSendPdfEmail}
|
||||
onDownloadWordExcel={handleDownloadWordExcel}
|
||||
onSendWordExcelEmail={handleSendWordExcelEmail}
|
||||
pdfReport={pdfReport}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<EmailDialog
|
||||
isOpen={isEmailDialogOpen}
|
||||
onClose={() => {
|
||||
setIsEmailDialogOpen(false);
|
||||
setPendingEmailAction(null);
|
||||
}}
|
||||
onSubmit={handleEmailSubmit}
|
||||
isLoading={isEmailLoading}
|
||||
defaultSubject={
|
||||
pendingEmailAction?.type === 'pdf'
|
||||
? 'Отчет по вкладам и кредитным программам по валютам'
|
||||
: pendingEmailAction?.data.format === 'doc'
|
||||
? 'Word отчет по вкладам по кредитным программам'
|
||||
: 'Excel отчет по вкладам по кредитным программам'
|
||||
}
|
||||
defaultBody={
|
||||
pendingEmailAction?.type === 'pdf'
|
||||
? `Отчет по вкладам и кредитным программам по валютам за период с ${
|
||||
pendingEmailAction.data.fromDate
|
||||
? format(pendingEmailAction.data.fromDate, 'dd.MM.yyyy')
|
||||
: ''
|
||||
} по ${
|
||||
pendingEmailAction.data.toDate
|
||||
? format(pendingEmailAction.data.toDate, 'dd.MM.yyyy')
|
||||
: ''
|
||||
}`
|
||||
: 'В приложении находится отчет по вкладам по кредитным программам.'
|
||||
}
|
||||
/>
|
||||
<ReportViewer selectedReport={selectedReport} />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
152
TheBank/bankui/src/components/ui/EmailDialog.tsx
Normal file
152
TheBank/bankui/src/components/ui/EmailDialog.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import React from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
|
||||
const emailSchema = z.object({
|
||||
email: z.string().email('Введите корректный email адрес'),
|
||||
subject: z.string().min(1, 'Тема письма обязательна'),
|
||||
body: z.string().min(1, 'Текст письма обязателен'),
|
||||
});
|
||||
|
||||
type EmailFormData = z.infer<typeof emailSchema>;
|
||||
|
||||
interface EmailDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: EmailFormData) => void;
|
||||
isLoading?: boolean;
|
||||
defaultSubject?: string;
|
||||
defaultBody?: string;
|
||||
}
|
||||
|
||||
export const EmailDialog: React.FC<EmailDialogProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSubmit,
|
||||
isLoading = false,
|
||||
defaultSubject = '',
|
||||
defaultBody = '',
|
||||
}) => {
|
||||
const form = useForm<EmailFormData>({
|
||||
resolver: zodResolver(emailSchema),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
subject: defaultSubject,
|
||||
body: defaultBody,
|
||||
},
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isOpen) {
|
||||
form.reset({
|
||||
email: '',
|
||||
subject: defaultSubject,
|
||||
body: defaultBody,
|
||||
});
|
||||
}
|
||||
}, [isOpen, defaultSubject, defaultBody, form]);
|
||||
|
||||
const handleSubmit = (data: EmailFormData) => {
|
||||
onSubmit(data);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
form.reset();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Отправка отчета на почту</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
className="space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email адрес</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="example@example.com"
|
||||
type="email"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="subject"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Тема письма</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Тема письма" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="body"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Текст письма</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Текст письма..."
|
||||
className="min-h-[100px]"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isLoading ? 'Отправка...' : 'Отправить'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
36
TheBank/bankui/src/components/ui/badge.tsx
Normal file
36
TheBank/bankui/src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import * as React from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
|
||||
secondary:
|
||||
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
destructive:
|
||||
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
||||
outline: 'text-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
153
TheBank/bankui/src/components/ui/command.tsx
Normal file
153
TheBank/bankui/src/components/ui/command.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import * as React from 'react';
|
||||
import { type DialogProps } from '@radix-ui/react-dialog';
|
||||
import { Command as CommandPrimitive } from 'cmdk';
|
||||
import { Search } from 'lucide-react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog';
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Command.displayName = CommandPrimitive.displayName;
|
||||
|
||||
interface CommandDialogProps extends DialogProps {}
|
||||
|
||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName;
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 h-px bg-border', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'ml-auto text-xs tracking-widest text-muted-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
CommandShortcut.displayName = 'CommandShortcut';
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
};
|
||||
116
TheBank/bankui/src/components/ui/multi-select.tsx
Normal file
116
TheBank/bankui/src/components/ui/multi-select.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import React from 'react';
|
||||
import { Check, ChevronsUpDown, X } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from '@/components/ui/command';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
export interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface MultiSelectProps {
|
||||
options: Option[];
|
||||
value: string[];
|
||||
onChange: (value: string[]) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const MultiSelect: React.FC<MultiSelectProps> = ({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
}) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const selectedLabels = value.map(
|
||||
(v) => options.find((opt) => opt.value === v)?.label,
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn('w-full justify-between', className)}
|
||||
>
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{selectedLabels.length > 0 ? (
|
||||
selectedLabels.map(
|
||||
(label, i) =>
|
||||
label && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
key={label}
|
||||
className="mr-1 mb-1"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const newValue = value.filter(
|
||||
(_, index) => index !== i,
|
||||
);
|
||||
onChange(newValue);
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-4 w-4 p-0 ml-1 hover:bg-transparent"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</Badge>
|
||||
),
|
||||
)
|
||||
) : (
|
||||
<span className="text-muted-foreground">Выберите опции...</span>
|
||||
)}
|
||||
</div>
|
||||
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Поиск..." />
|
||||
<CommandEmpty>Ничего не найдено.</CommandEmpty>
|
||||
<CommandGroup className="max-h-64 overflow-auto">
|
||||
{options.map((option) => (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
onSelect={() => {
|
||||
const newValue = value.includes(option.value)
|
||||
? value.filter((v) => v !== option.value)
|
||||
: [...value, option.value];
|
||||
onChange(newValue);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4',
|
||||
value.includes(option.value) ? 'opacity-100' : 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
{option.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
42
TheBank/bankui/src/components/ui/radio-group.tsx
Normal file
42
TheBank/bankui/src/components/ui/radio-group.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import * as React from 'react';
|
||||
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
|
||||
import { Circle } from 'lucide-react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const RadioGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
className={cn('grid gap-2', className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
});
|
||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
||||
|
||||
const RadioGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
);
|
||||
});
|
||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
||||
|
||||
export { RadioGroup, RadioGroupItem };
|
||||
22
TheBank/bankui/src/components/ui/textarea.tsx
Normal file
22
TheBank/bankui/src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Textarea = React.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
React.TextareaHTMLAttributes<HTMLTextAreaElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Textarea.displayName = 'Textarea';
|
||||
|
||||
export { Textarea };
|
||||
@@ -1,39 +1,20 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { creditProgramsApi } from '@/api/api';
|
||||
import type { CreditProgramBindingModel } from '@/types/types';
|
||||
|
||||
export const useCreditPrograms = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
data: creditPrograms,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery({
|
||||
} = useQuery<CreditProgramBindingModel[]>({
|
||||
queryKey: ['creditPrograms'],
|
||||
queryFn: creditProgramsApi.getAll,
|
||||
});
|
||||
|
||||
const { mutate: createCreditProgram } = useMutation({
|
||||
mutationFn: creditProgramsApi.create,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['creditPrograms'] });
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: updateCreditProgram } = useMutation({
|
||||
mutationFn: creditProgramsApi.update,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['creditPrograms'] });
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
creditPrograms,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
createCreditProgram,
|
||||
updateCreditProgram,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -108,3 +108,21 @@ export interface MailSendInfoBindingModel {
|
||||
body: string;
|
||||
attachmentPath?: string;
|
||||
}
|
||||
|
||||
export interface ReportMailSendInfoBindingModel
|
||||
extends MailSendInfoBindingModel {
|
||||
email: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface CreditProgramReportMailSendInfoBindingModel
|
||||
extends ReportMailSendInfoBindingModel {
|
||||
creditProgramIds: string[];
|
||||
}
|
||||
|
||||
export interface DepositReportMailSendInfoBindingModel
|
||||
extends ReportMailSendInfoBindingModel {
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
}
|
||||
|
||||
@@ -9,32 +9,14 @@ export default defineConfig({
|
||||
server: {
|
||||
port: 26312,
|
||||
cors: true,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
fs: {
|
||||
allow: ['..', './public'],
|
||||
},
|
||||
},
|
||||
|
||||
define: {
|
||||
global: 'globalThis',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'pdfjs-dist': path.resolve(
|
||||
__dirname,
|
||||
'./node_modules/pdfjs-dist/build/pdf',
|
||||
),
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['react-pdf', 'pdfjs-dist'],
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
pdfjs: ['pdfjs-dist'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
postData,
|
||||
postLoginData,
|
||||
putData,
|
||||
getFileData,
|
||||
postEmailData,
|
||||
} from './client';
|
||||
import type {
|
||||
ClientBindingModel,
|
||||
@@ -133,3 +135,72 @@ export const storekeepersApi = {
|
||||
getCurrentUser: () =>
|
||||
getSingleData<StorekeeperBindingModel>('api/storekeepers/me'),
|
||||
};
|
||||
|
||||
// Reports API
|
||||
export const reportsApi = {
|
||||
// PDF отчеты по депозитам
|
||||
getDepositsPdfReport: (fromDate: string, toDate: string) =>
|
||||
getFileData(
|
||||
`api/Report/LoadClientsByDeposit?fromDate=${fromDate}&toDate=${toDate}`,
|
||||
),
|
||||
|
||||
getDepositsDataReport: (fromDate: string, toDate: string) =>
|
||||
getData(
|
||||
`api/Report/GetClientByDeposit?fromDate=${fromDate}&toDate=${toDate}`,
|
||||
),
|
||||
|
||||
sendDepositsPdfReport: (
|
||||
fromDate: string,
|
||||
toDate: string,
|
||||
email: string,
|
||||
subject: string,
|
||||
body: string,
|
||||
) =>
|
||||
postEmailData(
|
||||
`api/Report/SendReportByDeposit?fromDate=${fromDate}&toDate=${toDate}`,
|
||||
{ email, subject, body },
|
||||
),
|
||||
|
||||
// Word отчеты по кредитным программам
|
||||
getCreditProgramsWordReport: (creditProgramIds: string) =>
|
||||
getFileData(`api/Report/LoadClientsByCreditProgram?${creditProgramIds}`),
|
||||
|
||||
getCreditProgramsDataReport: (creditProgramIds: string[]) =>
|
||||
getData(
|
||||
`api/Report/GetClientByCreditProgram?creditProgramIds=${creditProgramIds.join(
|
||||
',',
|
||||
)}`,
|
||||
),
|
||||
|
||||
sendCreditProgramsWordReport: (
|
||||
creditProgramIds: string[],
|
||||
email: string,
|
||||
subject: string,
|
||||
body: string,
|
||||
) =>
|
||||
postEmailData('api/Report/SendReportByCreditProgram', {
|
||||
email,
|
||||
subject,
|
||||
body,
|
||||
creditProgramIds,
|
||||
}),
|
||||
|
||||
// Excel отчеты по кредитным программам
|
||||
getCreditProgramsExcelReport: (creditProgramIds: string) =>
|
||||
getFileData(
|
||||
`api/Report/LoadExcelClientByCreditProgram?${creditProgramIds}`,
|
||||
),
|
||||
|
||||
sendCreditProgramsExcelReport: (
|
||||
creditProgramIds: string[],
|
||||
email: string,
|
||||
subject: string,
|
||||
body: string,
|
||||
) =>
|
||||
postEmailData('api/Report/SendExcelReportByCreditProgram', {
|
||||
email,
|
||||
subject,
|
||||
body,
|
||||
creditProgramIds,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -69,3 +69,66 @@ export async function putData<T>(path: string, data: T) {
|
||||
throw new Error(`Не получается загрузить ${path}: ${res.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для получения файлов отчетов
|
||||
export async function getFileData(path: string): Promise<{
|
||||
blob: Blob;
|
||||
fileName: string;
|
||||
mimeType: string;
|
||||
}> {
|
||||
const res = await fetch(`${API_URL}/${path}`, {
|
||||
credentials: 'include',
|
||||
// Убираем заголовок Content-Type для GET запросов файлов
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Не получается загрузить файл ${path}: ${res.statusText}`);
|
||||
}
|
||||
|
||||
const blob = await res.blob();
|
||||
const contentDisposition = res.headers.get('Content-Disposition');
|
||||
const contentType =
|
||||
res.headers.get('Content-Type') || 'application/octet-stream';
|
||||
|
||||
let fileName = 'report';
|
||||
if (contentDisposition) {
|
||||
const fileNameMatch = contentDisposition.match(
|
||||
/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/,
|
||||
);
|
||||
if (fileNameMatch) {
|
||||
fileName = fileNameMatch[1].replace(/['"]/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
// Если имя файла не извлечено из заголовка, пытаемся определить по URL
|
||||
if (fileName === 'report') {
|
||||
if (path.includes('LoadClientsByCreditProgram')) {
|
||||
fileName = 'clientsbycreditprogram.docx';
|
||||
} else if (path.includes('LoadExcelClientByCreditProgram')) {
|
||||
fileName = 'clientsbycreditprogram.xlsx';
|
||||
} else if (path.includes('LoadClientsByDeposit')) {
|
||||
fileName = 'clientbydeposit.pdf';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
blob,
|
||||
fileName,
|
||||
mimeType: contentType,
|
||||
};
|
||||
}
|
||||
|
||||
// Функция для отправки email с отчетами
|
||||
export async function postEmailData<T>(path: string, data: 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(`Не получается отправить email ${path}: ${res.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
159
TheBank/bankuiclerk/src/components/features/EmailDialog.tsx
Normal file
159
TheBank/bankuiclerk/src/components/features/EmailDialog.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import React from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
const emailSchema = z.object({
|
||||
email: z.string().email('Введите корректный email адрес'),
|
||||
subject: z.string().min(1, 'Введите тему письма'),
|
||||
body: z.string().min(1, 'Введите текст письма'),
|
||||
});
|
||||
|
||||
type EmailFormData = z.infer<typeof emailSchema>;
|
||||
|
||||
interface EmailDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: EmailFormData) => void;
|
||||
isLoading?: boolean;
|
||||
title?: string;
|
||||
description?: string;
|
||||
defaultSubject?: string;
|
||||
defaultBody?: string;
|
||||
}
|
||||
|
||||
export const EmailDialog = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSubmit,
|
||||
isLoading = false,
|
||||
title = 'Отправить отчет на почту',
|
||||
description = 'Заполните данные для отправки отчета',
|
||||
defaultSubject = 'Отчет из банковской системы',
|
||||
defaultBody = 'Во вложении находится запрошенный отчет.',
|
||||
}: EmailDialogProps) => {
|
||||
const form = useForm<EmailFormData>({
|
||||
resolver: zodResolver(emailSchema),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
subject: defaultSubject,
|
||||
body: defaultBody,
|
||||
},
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isOpen) {
|
||||
form.reset({
|
||||
email: '',
|
||||
subject: defaultSubject,
|
||||
body: defaultBody,
|
||||
});
|
||||
}
|
||||
}, [isOpen, form, defaultSubject, defaultBody]);
|
||||
|
||||
const handleSubmit = (data: EmailFormData) => {
|
||||
onSubmit(data);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
form.reset();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
className="space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email адрес</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="example@email.com"
|
||||
type="email"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="subject"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Тема письма</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Тема письма" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="body"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Текст письма</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Текст письма"
|
||||
className="min-h-[100px]"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isLoading ? 'Отправка...' : 'Отправить'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
115
TheBank/bankuiclerk/src/components/features/PdfViewer.tsx
Normal file
115
TheBank/bankuiclerk/src/components/features/PdfViewer.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
|
||||
export type ReportCategory = 'deposits' | 'creditPrograms';
|
||||
|
||||
interface ReportSidebarProps {
|
||||
selectedCategory: ReportCategory | null;
|
||||
onCategorySelect: (category: ReportCategory) => void;
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
export const ReportSidebar = ({
|
||||
selectedCategory,
|
||||
onCategorySelect,
|
||||
onReset,
|
||||
}: ReportSidebarProps) => {
|
||||
return (
|
||||
<div className="w-70 border-r bg-muted/10 p-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-3">Категории отчетов</h3>
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
variant={selectedCategory === 'deposits' ? 'default' : 'outline'}
|
||||
className="w-full"
|
||||
onClick={() => onCategorySelect('deposits')}
|
||||
>
|
||||
Отчеты по депозитам
|
||||
</Button>
|
||||
<Button
|
||||
variant={
|
||||
selectedCategory === 'creditPrograms' ? 'default' : 'outline'
|
||||
}
|
||||
className="w-full text-wrap p-5"
|
||||
onClick={() => onCategorySelect('creditPrograms')}
|
||||
>
|
||||
Отчеты по кредитным программам
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
onClick={onReset}
|
||||
disabled={!selectedCategory}
|
||||
>
|
||||
Сбросить выбор
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
530
TheBank/bankuiclerk/src/components/features/ReportViewer.tsx
Normal file
530
TheBank/bankuiclerk/src/components/features/ReportViewer.tsx
Normal file
@@ -0,0 +1,530 @@
|
||||
import React from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { format } from 'date-fns';
|
||||
import { ru } from 'date-fns/locale';
|
||||
import {
|
||||
CalendarIcon,
|
||||
FileText,
|
||||
Download,
|
||||
Mail,
|
||||
FileSpreadsheet,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { PdfViewer } from './PdfViewer';
|
||||
import { EmailDialog } from './EmailDialog';
|
||||
import { useCreditPrograms } from '@/hooks/useCreditPrograms';
|
||||
import type { ReportCategory } from './ReportSidebar';
|
||||
|
||||
// Схемы валидации
|
||||
const depositsReportSchema = z
|
||||
.object({
|
||||
fromDate: z.date({ required_error: 'Выберите дату начала' }),
|
||||
toDate: z.date({ required_error: 'Выберите дату окончания' }),
|
||||
})
|
||||
.refine((data) => data.fromDate <= data.toDate, {
|
||||
message: 'Дата начала должна быть раньше даты окончания',
|
||||
path: ['toDate'],
|
||||
});
|
||||
|
||||
const creditProgramsReportSchema = z.object({
|
||||
creditProgramIds: z
|
||||
.array(z.string())
|
||||
.min(1, 'Выберите хотя бы одну кредитную программу'),
|
||||
format: z.enum(['word', 'excel'], {
|
||||
required_error: 'Выберите формат отчета',
|
||||
}),
|
||||
});
|
||||
|
||||
type DepositsReportForm = z.infer<typeof depositsReportSchema>;
|
||||
type CreditProgramsReportForm = z.infer<typeof creditProgramsReportSchema>;
|
||||
|
||||
interface ReportViewerProps {
|
||||
category: ReportCategory | null;
|
||||
onGenerateReport: (type: string, data: Record<string, unknown>) => void;
|
||||
onDownloadReport: (type: string, data: Record<string, unknown>) => void;
|
||||
onSendEmail: (
|
||||
type: string,
|
||||
data: Record<string, unknown>,
|
||||
email: string,
|
||||
subject: string,
|
||||
body: string,
|
||||
) => void;
|
||||
pdfReport: { blob: Blob; fileName: string; mimeType: string } | null;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const ReportViewer = ({
|
||||
category,
|
||||
onGenerateReport,
|
||||
onDownloadReport,
|
||||
onSendEmail,
|
||||
pdfReport,
|
||||
isLoading,
|
||||
}: ReportViewerProps) => {
|
||||
const { creditPrograms } = useCreditPrograms();
|
||||
|
||||
// Состояние для EmailDialog
|
||||
const [isEmailDialogOpen, setIsEmailDialogOpen] = React.useState(false);
|
||||
const [emailDialogData, setEmailDialogData] = React.useState<{
|
||||
type: string;
|
||||
data: Record<string, unknown>;
|
||||
defaultSubject: string;
|
||||
defaultBody: string;
|
||||
} | null>(null);
|
||||
|
||||
// Формы для разных типов отчетов
|
||||
const depositsForm = useForm<DepositsReportForm>({
|
||||
resolver: zodResolver(depositsReportSchema),
|
||||
});
|
||||
|
||||
const creditProgramsForm = useForm<CreditProgramsReportForm>({
|
||||
resolver: zodResolver(creditProgramsReportSchema),
|
||||
defaultValues: {
|
||||
creditProgramIds: [],
|
||||
format: 'word',
|
||||
},
|
||||
});
|
||||
|
||||
// Обработчики для отчетов по депозитам
|
||||
const handleGenerateDepositsReport = (data: DepositsReportForm) => {
|
||||
onGenerateReport('deposits-pdf', {
|
||||
fromDate: format(data.fromDate, 'yyyy-MM-dd'),
|
||||
toDate: format(data.toDate, 'yyyy-MM-dd'),
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownloadDepositsReport = (data: DepositsReportForm) => {
|
||||
onDownloadReport('deposits-pdf', {
|
||||
fromDate: format(data.fromDate, 'yyyy-MM-dd'),
|
||||
toDate: format(data.toDate, 'yyyy-MM-dd'),
|
||||
});
|
||||
};
|
||||
|
||||
const handleSendDepositsEmail = (data: DepositsReportForm) => {
|
||||
setEmailDialogData({
|
||||
type: 'deposits-pdf',
|
||||
data: {
|
||||
fromDate: format(data.fromDate, 'yyyy-MM-dd'),
|
||||
toDate: format(data.toDate, 'yyyy-MM-dd'),
|
||||
},
|
||||
defaultSubject: 'Отчет по депозитам',
|
||||
defaultBody: `Отчет по депозитам за период с ${format(
|
||||
data.fromDate,
|
||||
'dd.MM.yyyy',
|
||||
)} по ${format(data.toDate, 'dd.MM.yyyy')}.`,
|
||||
});
|
||||
setIsEmailDialogOpen(true);
|
||||
};
|
||||
|
||||
// Обработчики для отчетов по кредитным программам
|
||||
const handleDownloadCreditProgramsReport = (
|
||||
data: CreditProgramsReportForm,
|
||||
) => {
|
||||
onDownloadReport(`creditPrograms-${data.format}`, {
|
||||
creditProgramIds: data.creditProgramIds,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSendCreditProgramsEmail = (data: CreditProgramsReportForm) => {
|
||||
const selectedPrograms = data.creditProgramIds
|
||||
.map((id) => creditPrograms?.find((p) => p.id === id)?.name)
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
|
||||
setEmailDialogData({
|
||||
type: `creditPrograms-${data.format}`,
|
||||
data: {
|
||||
creditProgramIds: data.creditProgramIds,
|
||||
},
|
||||
defaultSubject: `Отчет по кредитным программам (${data.format.toUpperCase()})`,
|
||||
defaultBody: `Отчет по кредитным программам: ${selectedPrograms}.`,
|
||||
});
|
||||
setIsEmailDialogOpen(true);
|
||||
};
|
||||
|
||||
// Проверка валидности форм
|
||||
const depositsFormData = depositsForm.watch();
|
||||
const isDepositsFormValid =
|
||||
depositsFormData.fromDate && depositsFormData.toDate;
|
||||
|
||||
const creditProgramsFormData = creditProgramsForm.watch();
|
||||
const isCreditProgramsFormValid =
|
||||
creditProgramsFormData.creditProgramIds?.length > 0;
|
||||
|
||||
// Обработка мультиселекта кредитных программ
|
||||
const selectedCreditProgramIds =
|
||||
creditProgramsForm.watch('creditProgramIds') || [];
|
||||
|
||||
const handleCreditProgramSelect = (creditProgramId: string) => {
|
||||
const currentValues = selectedCreditProgramIds;
|
||||
if (!currentValues.includes(creditProgramId)) {
|
||||
creditProgramsForm.setValue('creditProgramIds', [
|
||||
...currentValues,
|
||||
creditProgramId,
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreditProgramRemove = (creditProgramId: string) => {
|
||||
const newValues = selectedCreditProgramIds.filter(
|
||||
(id) => id !== creditProgramId,
|
||||
);
|
||||
creditProgramsForm.setValue('creditProgramIds', newValues);
|
||||
};
|
||||
|
||||
// Обработчик отправки email
|
||||
const handleEmailSubmit = (emailData: {
|
||||
email: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
}) => {
|
||||
if (emailDialogData) {
|
||||
onSendEmail(
|
||||
emailDialogData.type,
|
||||
emailDialogData.data,
|
||||
emailData.email,
|
||||
emailData.subject,
|
||||
emailData.body,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (!category) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl font-bold mb-2">Выберите категорию отчета</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Используйте боковую панель для выбора типа отчета
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 p-6">
|
||||
{category === 'deposits' && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-2">Отчеты по вкладам</h2>
|
||||
<p className="text-muted-foreground">
|
||||
PDF отчет с информацией о клиентах по вкладам за выбранный период
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Form {...depositsForm}>
|
||||
<form className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={depositsForm.control}
|
||||
name="fromDate"
|
||||
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', { locale: ru })
|
||||
) : (
|
||||
<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}
|
||||
disabled={(date) =>
|
||||
date > new Date() || date < new Date('1900-01-01')
|
||||
}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={depositsForm.control}
|
||||
name="toDate"
|
||||
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', { locale: ru })
|
||||
) : (
|
||||
<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}
|
||||
disabled={(date) =>
|
||||
date > new Date() || date < new Date('1900-01-01')
|
||||
}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={depositsForm.handleSubmit(
|
||||
handleGenerateDepositsReport,
|
||||
)}
|
||||
disabled={!isDepositsFormValid || isLoading}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
Сгенерировать на странице
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={depositsForm.handleSubmit(
|
||||
handleDownloadDepositsReport,
|
||||
)}
|
||||
disabled={!isDepositsFormValid || isLoading}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Скачать
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={depositsForm.handleSubmit(handleSendDepositsEmail)}
|
||||
disabled={!isDepositsFormValid || isLoading}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Mail className="h-4 w-4" />
|
||||
Отправить на почту
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{pdfReport && (
|
||||
<div className="border rounded-lg">
|
||||
<PdfViewer report={pdfReport} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{category === 'creditPrograms' && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-2">
|
||||
Отчеты по кредитным программам
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Word или Excel отчет с информацией о клиентах по выбранным
|
||||
кредитным программам
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Form {...creditProgramsForm}>
|
||||
<form className="space-y-4">
|
||||
<FormField
|
||||
control={creditProgramsForm.control}
|
||||
name="format"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Формат отчета</FormLabel>
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant={field.value === 'word' ? 'default' : 'outline'}
|
||||
onClick={() => field.onChange('word')}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
Word
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={
|
||||
field.value === 'excel' ? 'default' : 'outline'
|
||||
}
|
||||
onClick={() => field.onChange('excel')}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<FileSpreadsheet className="h-4 w-4" />
|
||||
Excel
|
||||
</Button>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={creditProgramsForm.control}
|
||||
name="creditProgramIds"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>Кредитные программы</FormLabel>
|
||||
<Select onValueChange={handleCreditProgramSelect}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Выберите кредитные программы" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<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-2">
|
||||
{selectedCreditProgramIds.map((programId) => {
|
||||
const program = creditPrograms?.find(
|
||||
(p) => p.id === programId,
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={programId}
|
||||
className="bg-muted px-2 py-1 rounded-md flex items-center gap-1"
|
||||
>
|
||||
<span>{program?.name || programId}</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-4 w-4 rounded-full"
|
||||
onClick={() =>
|
||||
handleCreditProgramRemove(programId)
|
||||
}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={creditProgramsForm.handleSubmit(
|
||||
handleDownloadCreditProgramsReport,
|
||||
)}
|
||||
disabled={!isCreditProgramsFormValid || isLoading}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Скачать
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={creditProgramsForm.handleSubmit(
|
||||
handleSendCreditProgramsEmail,
|
||||
)}
|
||||
disabled={!isCreditProgramsFormValid || isLoading}
|
||||
className="flex flex-col items-center gap-1 h-auto py-2 px-3 min-w-[100px]"
|
||||
>
|
||||
<Mail className="h-4 w-4" />
|
||||
<span className="text-xs leading-tight text-center">
|
||||
Отправить на почту
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Email Dialog */}
|
||||
<EmailDialog
|
||||
isOpen={isEmailDialogOpen}
|
||||
onClose={() => setIsEmailDialogOpen(false)}
|
||||
onSubmit={handleEmailSubmit}
|
||||
isLoading={isLoading}
|
||||
title={emailDialogData?.defaultSubject || 'Отправить отчет на почту'}
|
||||
description="Заполните данные для отправки отчета"
|
||||
defaultSubject={
|
||||
emailDialogData?.defaultSubject || 'Отчет из банковской системы'
|
||||
}
|
||||
defaultBody={
|
||||
emailDialogData?.defaultBody ||
|
||||
'Во вложении находится запрошенный отчет.'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -71,6 +71,16 @@ const navOptions = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Отчеты',
|
||||
options: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Выгрузить отчеты',
|
||||
link: '/reports',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const Header = (): React.JSX.Element => {
|
||||
|
||||
@@ -77,12 +77,10 @@ export const Clients = (): React.JSX.Element => {
|
||||
return clients.map((client) => {
|
||||
const clerk = clerks.find((c) => c.id === client.clerkId);
|
||||
|
||||
// Находим вклады клиента
|
||||
const clientDeposits = deposits?.filter(() => {
|
||||
// Учитывая, что мы удалили depositClients из модели, эта проверка будет всегда возвращать false
|
||||
// Здесь нужно реализовать другой способ связи, или просто удалить эту функциональность
|
||||
return false; // Больше не можем определить связь через deposit.depositClients
|
||||
});
|
||||
const clientDeposits =
|
||||
deposits?.filter((deposit) =>
|
||||
client.depositClients?.some((dc) => dc.depositId === deposit.id),
|
||||
) || [];
|
||||
|
||||
// Находим кредитные программы клиента
|
||||
const clientCreditPrograms = creditPrograms.filter((creditProgram) =>
|
||||
@@ -91,12 +89,9 @@ export const Clients = (): React.JSX.Element => {
|
||||
),
|
||||
);
|
||||
|
||||
// Формируем строки с информацией о вкладах и кредитах
|
||||
const depositsList =
|
||||
clientDeposits.length > 0
|
||||
? clientDeposits
|
||||
.map((d) => `${d.interestRate}% (${d.period} мес.)`)
|
||||
.join(', ')
|
||||
clientDeposits && clientDeposits.length > 0
|
||||
? clientDeposits.map((d) => `Вклад ${d.interestRate}%`).join(', ')
|
||||
: 'Нет вкладов';
|
||||
|
||||
const creditProgramsList =
|
||||
|
||||
259
TheBank/bankuiclerk/src/components/pages/Reports.tsx
Normal file
259
TheBank/bankuiclerk/src/components/pages/Reports.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
import React from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
ReportSidebar,
|
||||
type ReportCategory,
|
||||
} from '@/components/features/ReportSidebar';
|
||||
import { ReportViewer } from '@/components/features/ReportViewer';
|
||||
import { useReports } from '@/hooks/useReports';
|
||||
|
||||
export const Reports = (): React.JSX.Element => {
|
||||
const [selectedCategory, setSelectedCategory] =
|
||||
React.useState<ReportCategory | null>(null);
|
||||
const [pdfReport, setPdfReport] = React.useState<{
|
||||
blob: Blob;
|
||||
fileName: string;
|
||||
mimeType: string;
|
||||
} | null>(null);
|
||||
|
||||
const {
|
||||
generateDepositsPdfReport,
|
||||
isGeneratingDepositsPdf,
|
||||
sendDepositsPdfReport,
|
||||
isSendingDepositsPdf,
|
||||
generateCreditProgramsWordReport,
|
||||
isGeneratingCreditProgramsWord,
|
||||
sendCreditProgramsWordReport,
|
||||
isSendingCreditProgramsWord,
|
||||
generateCreditProgramsExcelReport,
|
||||
isGeneratingCreditProgramsExcel,
|
||||
sendCreditProgramsExcelReport,
|
||||
isSendingCreditProgramsExcel,
|
||||
} = useReports();
|
||||
|
||||
const isLoading =
|
||||
isGeneratingDepositsPdf ||
|
||||
isSendingDepositsPdf ||
|
||||
isGeneratingCreditProgramsWord ||
|
||||
isSendingCreditProgramsWord ||
|
||||
isGeneratingCreditProgramsExcel ||
|
||||
isSendingCreditProgramsExcel;
|
||||
|
||||
const handleCategorySelect = (category: ReportCategory) => {
|
||||
setSelectedCategory(category);
|
||||
setPdfReport(null); // Сбрасываем PDF при смене категории
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSelectedCategory(null);
|
||||
setPdfReport(null);
|
||||
};
|
||||
|
||||
const downloadFile = (blob: Blob, fileName: string, mimeType?: string) => {
|
||||
// Просто используем имя файла как есть, если оно уже содержит расширение
|
||||
let finalFileName = fileName;
|
||||
|
||||
// Проверяем, есть ли уже расширение в имени файла
|
||||
const hasExtension = /\.(docx|xlsx|pdf|doc|xls)$/i.test(fileName);
|
||||
|
||||
if (!hasExtension) {
|
||||
// Только если нет расширения, пытаемся его добавить
|
||||
if (mimeType && mimeType !== 'application/octet-stream') {
|
||||
const extension = getExtensionFromMimeType(mimeType);
|
||||
if (extension) {
|
||||
finalFileName = `${fileName}${extension}`;
|
||||
}
|
||||
} else {
|
||||
// Fallback: определяем по имени файла
|
||||
const extension = getExtensionFromFileName(fileName);
|
||||
if (extension) {
|
||||
finalFileName = `${fileName}${extension}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = finalFileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const getExtensionFromMimeType = (mimeType: string): string => {
|
||||
switch (mimeType.toLowerCase()) {
|
||||
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
|
||||
return '.docx';
|
||||
case 'application/msword':
|
||||
return '.doc';
|
||||
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
|
||||
return '.xlsx';
|
||||
case 'application/vnd.ms-excel':
|
||||
return '.xls';
|
||||
case 'application/pdf':
|
||||
return '.pdf';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const getExtensionFromFileName = (fileName: string): string => {
|
||||
const lowerFileName = fileName.toLowerCase();
|
||||
if (lowerFileName.includes('clientsbycreditprogram')) {
|
||||
return '.docx'; // Word файл для клиентов по кредитным программам
|
||||
}
|
||||
if (lowerFileName.includes('excel') || lowerFileName.includes('.xlsx')) {
|
||||
return '.xlsx';
|
||||
}
|
||||
if (lowerFileName.includes('pdf') || lowerFileName.includes('deposit')) {
|
||||
return '.pdf';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const handleGenerateReport = (
|
||||
type: string,
|
||||
data: Record<string, unknown>,
|
||||
) => {
|
||||
if (type === 'deposits-pdf') {
|
||||
const { fromDate, toDate } = data as { fromDate: string; toDate: string };
|
||||
generateDepositsPdfReport(
|
||||
{ fromDate, toDate },
|
||||
{
|
||||
onSuccess: (report) => {
|
||||
setPdfReport(report);
|
||||
toast.success('PDF отчет успешно сгенерирован');
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Ошибка генерации PDF отчета:', error);
|
||||
toast.error('Ошибка при генерации PDF отчета');
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadReport = (
|
||||
type: string,
|
||||
data: Record<string, unknown>,
|
||||
) => {
|
||||
if (type === 'deposits-pdf') {
|
||||
const { fromDate, toDate } = data as { fromDate: string; toDate: string };
|
||||
generateDepositsPdfReport(
|
||||
{ fromDate, toDate },
|
||||
{
|
||||
onSuccess: (report) => {
|
||||
downloadFile(report.blob, report.fileName, report.mimeType);
|
||||
toast.success('PDF отчет успешно скачан');
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Ошибка скачивания PDF отчета:', error);
|
||||
toast.error('Ошибка при скачивании PDF отчета');
|
||||
},
|
||||
},
|
||||
);
|
||||
} else if (type === 'creditPrograms-word') {
|
||||
const { creditProgramIds } = data as { creditProgramIds: string[] };
|
||||
generateCreditProgramsWordReport(
|
||||
{ creditProgramIds },
|
||||
{
|
||||
onSuccess: (report) => {
|
||||
downloadFile(report.blob, report.fileName, report.mimeType);
|
||||
toast.success('Word отчет успешно скачан');
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Ошибка скачивания Word отчета:', error);
|
||||
toast.error('Ошибка при скачивании Word отчета');
|
||||
},
|
||||
},
|
||||
);
|
||||
} else if (type === 'creditPrograms-excel') {
|
||||
const { creditProgramIds } = data as { creditProgramIds: string[] };
|
||||
generateCreditProgramsExcelReport(
|
||||
{ creditProgramIds },
|
||||
{
|
||||
onSuccess: (report) => {
|
||||
downloadFile(report.blob, report.fileName, report.mimeType);
|
||||
toast.success('Excel отчет успешно скачан');
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Ошибка скачивания Excel отчета:', error);
|
||||
toast.error('Ошибка при скачивании Excel отчета');
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendEmail = (
|
||||
type: string,
|
||||
data: Record<string, unknown>,
|
||||
email: string,
|
||||
subject: string,
|
||||
body: string,
|
||||
) => {
|
||||
if (type === 'deposits-pdf') {
|
||||
const { fromDate, toDate } = data as { fromDate: string; toDate: string };
|
||||
sendDepositsPdfReport(
|
||||
{ fromDate, toDate, email, subject, body },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(`PDF отчет успешно отправлен на ${email}`);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Ошибка отправки PDF отчета:', error);
|
||||
toast.error('Ошибка при отправке PDF отчета на email');
|
||||
},
|
||||
},
|
||||
);
|
||||
} else if (type === 'creditPrograms-word') {
|
||||
const { creditProgramIds } = data as { creditProgramIds: string[] };
|
||||
sendCreditProgramsWordReport(
|
||||
{ creditProgramIds, email, subject, body },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(`Word отчет успешно отправлен на ${email}`);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Ошибка отправки Word отчета:', error);
|
||||
toast.error('Ошибка при отправке Word отчета на email');
|
||||
},
|
||||
},
|
||||
);
|
||||
} else if (type === 'creditPrograms-excel') {
|
||||
const { creditProgramIds } = data as { creditProgramIds: string[] };
|
||||
sendCreditProgramsExcelReport(
|
||||
{ creditProgramIds, email, subject, body },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(`Excel отчет успешно отправлен на ${email}`);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Ошибка отправки Excel отчета:', error);
|
||||
toast.error('Ошибка при отправке Excel отчета на email');
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="flex-1 flex relative">
|
||||
<ReportSidebar
|
||||
selectedCategory={selectedCategory}
|
||||
onCategorySelect={handleCategorySelect}
|
||||
onReset={handleReset}
|
||||
/>
|
||||
<ReportViewer
|
||||
category={selectedCategory}
|
||||
onGenerateReport={handleGenerateReport}
|
||||
onDownloadReport={handleDownloadReport}
|
||||
onSendEmail={handleSendEmail}
|
||||
pdfReport={pdfReport}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
22
TheBank/bankuiclerk/src/components/ui/textarea.tsx
Normal file
22
TheBank/bankuiclerk/src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Textarea.displayName = 'Textarea';
|
||||
|
||||
export { Textarea };
|
||||
152
TheBank/bankuiclerk/src/hooks/useReports.ts
Normal file
152
TheBank/bankuiclerk/src/hooks/useReports.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { reportsApi } from '@/api/api';
|
||||
|
||||
export const useReports = () => {
|
||||
// PDF отчеты по депозитам
|
||||
const {
|
||||
mutate: generateDepositsPdfReport,
|
||||
isPending: isGeneratingDepositsPdf,
|
||||
} = useMutation({
|
||||
mutationFn: ({ fromDate, toDate }: { fromDate: string; toDate: string }) =>
|
||||
reportsApi.getDepositsPdfReport(fromDate, toDate),
|
||||
});
|
||||
|
||||
const { mutate: getDepositsData, isPending: isLoadingDepositsData } =
|
||||
useMutation({
|
||||
mutationFn: ({
|
||||
fromDate,
|
||||
toDate,
|
||||
}: {
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
}) => reportsApi.getDepositsDataReport(fromDate, toDate),
|
||||
});
|
||||
|
||||
const { mutate: sendDepositsPdfReport, isPending: isSendingDepositsPdf } =
|
||||
useMutation({
|
||||
mutationFn: ({
|
||||
fromDate,
|
||||
toDate,
|
||||
email,
|
||||
subject,
|
||||
body,
|
||||
}: {
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
email: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
}) =>
|
||||
reportsApi.sendDepositsPdfReport(
|
||||
fromDate,
|
||||
toDate,
|
||||
email,
|
||||
subject,
|
||||
body,
|
||||
),
|
||||
});
|
||||
|
||||
// Word отчеты по кредитным программам
|
||||
const {
|
||||
mutate: generateCreditProgramsWordReport,
|
||||
isPending: isGeneratingCreditProgramsWord,
|
||||
} = useMutation({
|
||||
mutationFn: ({ creditProgramIds }: { creditProgramIds: string[] }) => {
|
||||
const cpIds = creditProgramIds.reduce((prev, curr, index) => {
|
||||
return (prev += `${index === 0 ? '' : '&'}creditProgramIds=${curr}`);
|
||||
}, '');
|
||||
return reportsApi.getCreditProgramsWordReport(cpIds); // не при каких обстоятельствах не менять
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: getCreditProgramsData,
|
||||
isPending: isLoadingCreditProgramsData,
|
||||
} = useMutation({
|
||||
mutationFn: ({ creditProgramIds }: { creditProgramIds: string[] }) =>
|
||||
reportsApi.getCreditProgramsDataReport(creditProgramIds),
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: sendCreditProgramsWordReport,
|
||||
isPending: isSendingCreditProgramsWord,
|
||||
} = useMutation({
|
||||
mutationFn: ({
|
||||
creditProgramIds,
|
||||
email,
|
||||
subject,
|
||||
body,
|
||||
}: {
|
||||
creditProgramIds: string[];
|
||||
email: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
}) =>
|
||||
reportsApi.sendCreditProgramsWordReport(
|
||||
creditProgramIds,
|
||||
email,
|
||||
subject,
|
||||
body,
|
||||
),
|
||||
});
|
||||
|
||||
// Excel отчеты по кредитным программам
|
||||
const {
|
||||
mutate: generateCreditProgramsExcelReport,
|
||||
isPending: isGeneratingCreditProgramsExcel,
|
||||
} = useMutation({
|
||||
mutationFn: ({ creditProgramIds }: { creditProgramIds: string[] }) => {
|
||||
const cpIds = creditProgramIds.reduce((prev, curr, index) => {
|
||||
return (prev += `${index === 0 ? '' : '&'}creditProgramIds=${curr}`);
|
||||
}, '');
|
||||
return reportsApi.getCreditProgramsExcelReport(cpIds);
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: sendCreditProgramsExcelReport,
|
||||
isPending: isSendingCreditProgramsExcel,
|
||||
} = useMutation({
|
||||
mutationFn: ({
|
||||
creditProgramIds,
|
||||
email,
|
||||
subject,
|
||||
body,
|
||||
}: {
|
||||
creditProgramIds: string[];
|
||||
email: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
}) =>
|
||||
reportsApi.sendCreditProgramsExcelReport(
|
||||
creditProgramIds,
|
||||
email,
|
||||
subject,
|
||||
body,
|
||||
),
|
||||
});
|
||||
|
||||
return {
|
||||
// PDF отчеты по депозитам
|
||||
generateDepositsPdfReport,
|
||||
isGeneratingDepositsPdf,
|
||||
getDepositsData,
|
||||
isLoadingDepositsData,
|
||||
sendDepositsPdfReport,
|
||||
isSendingDepositsPdf,
|
||||
|
||||
// Word отчеты по кредитным программам
|
||||
generateCreditProgramsWordReport,
|
||||
isGeneratingCreditProgramsWord,
|
||||
getCreditProgramsData,
|
||||
isLoadingCreditProgramsData,
|
||||
sendCreditProgramsWordReport,
|
||||
isSendingCreditProgramsWord,
|
||||
|
||||
// Excel отчеты по кредитным программам
|
||||
generateCreditProgramsExcelReport,
|
||||
isGeneratingCreditProgramsExcel,
|
||||
sendCreditProgramsExcelReport,
|
||||
isSendingCreditProgramsExcel,
|
||||
};
|
||||
};
|
||||
@@ -15,6 +15,7 @@ import { Clerks } from './components/pages/Clerks.tsx';
|
||||
import { Clients } from './components/pages/Clients.tsx';
|
||||
import { Deposits } from './components/pages/Deposits.tsx';
|
||||
import { Replenishments } from './components/pages/Replenishments.tsx';
|
||||
import { Reports } from './components/pages/Reports.tsx';
|
||||
|
||||
const routes = createBrowserRouter([
|
||||
{
|
||||
@@ -41,6 +42,10 @@ const routes = createBrowserRouter([
|
||||
path: '/replenishments',
|
||||
element: <Replenishments />,
|
||||
},
|
||||
{
|
||||
path: '/reports',
|
||||
element: <Reports />,
|
||||
},
|
||||
],
|
||||
errorElement: <p>бля пизда рулям</p>,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user