feat: отчеты для клерка
This commit is contained in:
@@ -76,20 +76,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 +180,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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
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.
|
||||
@@ -50,8 +50,8 @@ export const reportsApi = {
|
||||
|
||||
// Word отчеты
|
||||
getWordReport: async (creditProgramIds: string[]) => {
|
||||
const idsParam = creditProgramIds.reduce((prev, curr) => {
|
||||
return (prev += `&creditProgramIds=${curr}`);
|
||||
const idsParam = creditProgramIds.reduce((prev, curr, index) => {
|
||||
return (prev += `${index === 0 ? '' : '&'}creditProgramIds=${curr}`);
|
||||
}, '');
|
||||
console.log('idsParam', idsParam);
|
||||
const res = await fetch(
|
||||
@@ -91,11 +91,11 @@ export const reportsApi = {
|
||||
|
||||
// Excel отчеты
|
||||
getExcelReport: async (creditProgramIds: string[]) => {
|
||||
const idsParam = creditProgramIds.reduce((prev, curr) => {
|
||||
return (prev += `&creditProgramIds=${curr}`);
|
||||
const idsParam = creditProgramIds.reduce((prev, curr, index) => {
|
||||
return (prev += `${index === 0 ? '' : '&'}creditProgramIds=${curr}`);
|
||||
}, '');
|
||||
const res = await fetch(
|
||||
`${API_URL}/api/Report/LoadExcelDepositByCreditProgram?creditProgramIds=${idsParam}`,
|
||||
`${API_URL}/api/Report/LoadExcelDepositByCreditProgram?${idsParam}`,
|
||||
{
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
postData,
|
||||
postLoginData,
|
||||
putData,
|
||||
getFileData,
|
||||
postEmailData,
|
||||
} from './client';
|
||||
import type {
|
||||
ClientBindingModel,
|
||||
@@ -133,3 +135,52 @@ 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) =>
|
||||
postEmailData(
|
||||
`api/Report/SendReportByDeposit?fromDate=${fromDate}&toDate=${toDate}`,
|
||||
{ email },
|
||||
),
|
||||
|
||||
// Word отчеты по кредитным программам
|
||||
getCreditProgramsWordReport: (creditProgramIds: string) =>
|
||||
getFileData(`api/Report/LoadClientsByCreditProgram?${creditProgramIds}`),
|
||||
|
||||
getCreditProgramsDataReport: (creditProgramIds: string[]) =>
|
||||
getData(
|
||||
`api/Report/GetClientByCreditProgram?creditProgramIds=${creditProgramIds.join(
|
||||
',',
|
||||
)}`,
|
||||
),
|
||||
|
||||
sendCreditProgramsWordReport: (creditProgramIds: string[], email: string) =>
|
||||
postEmailData('api/Report/SendReportByCreditProgram', {
|
||||
email,
|
||||
creditProgramIds,
|
||||
}),
|
||||
|
||||
// Excel отчеты по кредитным программам
|
||||
getCreditProgramsExcelReport: (creditProgramIds: string) =>
|
||||
getFileData(
|
||||
`api/Report/LoadExcelClientByCreditProgram?${creditProgramIds}`,
|
||||
),
|
||||
|
||||
sendCreditProgramsExcelReport: (creditProgramIds: string[], email: string) =>
|
||||
postEmailData('api/Report/SendExcelReportByCreditProgram', {
|
||||
email,
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
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-64 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 justify-start"
|
||||
onClick={() => onCategorySelect('deposits')}
|
||||
>
|
||||
Отчеты по депозитам
|
||||
</Button>
|
||||
<Button
|
||||
variant={
|
||||
selectedCategory === 'creditPrograms' ? 'default' : 'outline'
|
||||
}
|
||||
className="w-full justify-start"
|
||||
onClick={() => onCategorySelect('creditPrograms')}
|
||||
>
|
||||
Отчеты по кредитным программам
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
onClick={onReset}
|
||||
disabled={!selectedCategory}
|
||||
>
|
||||
Сбросить выбор
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
481
TheBank/bankuiclerk/src/components/features/ReportViewer.tsx
Normal file
481
TheBank/bankuiclerk/src/components/features/ReportViewer.tsx
Normal file
@@ -0,0 +1,481 @@
|
||||
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 { 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,
|
||||
) => void;
|
||||
pdfReport: { blob: Blob; fileName: string; mimeType: string } | null;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const ReportViewer = ({
|
||||
category,
|
||||
onGenerateReport,
|
||||
onDownloadReport,
|
||||
onSendEmail,
|
||||
pdfReport,
|
||||
isLoading,
|
||||
}: ReportViewerProps) => {
|
||||
const { creditPrograms } = useCreditPrograms();
|
||||
|
||||
// Формы для разных типов отчетов
|
||||
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, email: string) => {
|
||||
onSendEmail(
|
||||
'deposits-pdf',
|
||||
{
|
||||
fromDate: format(data.fromDate, 'yyyy-MM-dd'),
|
||||
toDate: format(data.toDate, 'yyyy-MM-dd'),
|
||||
},
|
||||
email,
|
||||
);
|
||||
};
|
||||
|
||||
// Обработчики для отчетов по кредитным программам
|
||||
const handleDownloadCreditProgramsReport = (
|
||||
data: CreditProgramsReportForm,
|
||||
) => {
|
||||
onDownloadReport(`creditPrograms-${data.format}`, {
|
||||
creditProgramIds: data.creditProgramIds,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSendCreditProgramsEmail = (
|
||||
data: CreditProgramsReportForm,
|
||||
email: string,
|
||||
) => {
|
||||
onSendEmail(
|
||||
`creditPrograms-${data.format}`,
|
||||
{
|
||||
creditProgramIds: data.creditProgramIds,
|
||||
},
|
||||
email,
|
||||
);
|
||||
};
|
||||
|
||||
// Проверка валидности форм
|
||||
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);
|
||||
};
|
||||
|
||||
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((data) => {
|
||||
const email = prompt('Введите email для отправки:');
|
||||
if (email) {
|
||||
handleSendDepositsEmail(data, email);
|
||||
}
|
||||
})}
|
||||
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((data) => {
|
||||
const email = prompt('Введите email для отправки:');
|
||||
if (email) {
|
||||
handleSendCreditProgramsEmail(data, email);
|
||||
}
|
||||
})}
|
||||
disabled={!isCreditProgramsFormValid || isLoading}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Mail className="h-4 w-4" />
|
||||
Отправить на почту
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
)}
|
||||
</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 =
|
||||
|
||||
257
TheBank/bankuiclerk/src/components/pages/Reports.tsx
Normal file
257
TheBank/bankuiclerk/src/components/pages/Reports.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
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,
|
||||
) => {
|
||||
if (type === 'deposits-pdf') {
|
||||
const { fromDate, toDate } = data as { fromDate: string; toDate: string };
|
||||
sendDepositsPdfReport(
|
||||
{ fromDate, toDate, email },
|
||||
{
|
||||
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 },
|
||||
{
|
||||
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 },
|
||||
{
|
||||
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>
|
||||
);
|
||||
};
|
||||
121
TheBank/bankuiclerk/src/hooks/useReports.ts
Normal file
121
TheBank/bankuiclerk/src/hooks/useReports.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
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,
|
||||
}: {
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
email: string;
|
||||
}) => reportsApi.sendDepositsPdfReport(fromDate, toDate, email),
|
||||
});
|
||||
|
||||
// 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,
|
||||
}: {
|
||||
creditProgramIds: string[];
|
||||
email: string;
|
||||
}) => reportsApi.sendCreditProgramsWordReport(creditProgramIds, email),
|
||||
});
|
||||
|
||||
// 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,
|
||||
}: {
|
||||
creditProgramIds: string[];
|
||||
email: string;
|
||||
}) => reportsApi.sendCreditProgramsExcelReport(creditProgramIds, email),
|
||||
});
|
||||
|
||||
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