feat: import VTB broker XLSX reports
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@family-budget/frontend",
|
||||
"version": "0.11.3",
|
||||
"version": "0.12.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { ImportPortfolioResponse, PortfolioFile } from '@family-budget/shared';
|
||||
import type { ImportBrokerReportResponse, ImportPortfolioResponse, PortfolioFile } from '@family-budget/shared';
|
||||
import { api } from './client';
|
||||
|
||||
export function importPortfolio(data: PortfolioFile): Promise<ImportPortfolioResponse> {
|
||||
return api.post('/api/import/portfolio', data);
|
||||
}
|
||||
|
||||
export function importBrokerReport(file: File): Promise<ImportBrokerReportResponse> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return api.postFormData('/api/import/broker', formData);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import type { ImportPortfolioResponse, ImportStatementResponse, PortfolioFile } from '@family-budget/shared';
|
||||
import type { ImportBrokerReportResponse, ImportPortfolioResponse, ImportStatementResponse, PortfolioFile } from '@family-budget/shared';
|
||||
import { importStatement } from '../api/import';
|
||||
import { importPortfolio } from '../api/portfolio';
|
||||
import { importBrokerReport, importPortfolio } from '../api/portfolio';
|
||||
import { updateAccount } from '../api/accounts';
|
||||
|
||||
interface Props {
|
||||
@@ -10,7 +10,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export function ImportModal({ onClose, onDone }: Props) {
|
||||
const [result, setResult] = useState<ImportStatementResponse | ImportPortfolioResponse | null>(null);
|
||||
const [result, setResult] = useState<ImportStatementResponse | ImportPortfolioResponse | ImportBrokerReportResponse | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [alias, setAlias] = useState('');
|
||||
@@ -27,9 +27,10 @@ export function ImportModal({ onClose, onDone }: Props) {
|
||||
const type = file.type;
|
||||
const isPdf = type === 'application/pdf' || name.endsWith('.pdf');
|
||||
const isJson = type === 'application/json' || name.endsWith('.json');
|
||||
const isXlsx = type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || name.endsWith('.xlsx');
|
||||
|
||||
if (!isPdf && !isJson) {
|
||||
setError('Допустимы только файлы PDF или JSON');
|
||||
if (!isPdf && !isJson && !isXlsx) {
|
||||
setError('Допустимы только файлы PDF, JSON или XLSX');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -39,7 +40,9 @@ export function ImportModal({ onClose, onDone }: Props) {
|
||||
|
||||
try {
|
||||
const data = isJson ? JSON.parse(await file.text()) : null;
|
||||
const resp = data?.schemaVersion === 'broker-portfolio-1.0'
|
||||
const resp = isXlsx
|
||||
? await importBrokerReport(file)
|
||||
: data?.schemaVersion === 'broker-portfolio-1.0'
|
||||
? await importPortfolio(data as PortfolioFile)
|
||||
: await importStatement(file);
|
||||
setResult(resp);
|
||||
@@ -53,7 +56,7 @@ export function ImportModal({ onClose, onDone }: Props) {
|
||||
};
|
||||
|
||||
const handleSaveAlias = async () => {
|
||||
if (!result || 'reportId' in result || !alias.trim()) return;
|
||||
if (!result || 'reportId' in result || 'cash' in result || !alias.trim()) return;
|
||||
try {
|
||||
await updateAccount(result.accountId, { alias: alias.trim() });
|
||||
setAliasSaved(true);
|
||||
@@ -63,6 +66,7 @@ export function ImportModal({ onClose, onDone }: Props) {
|
||||
};
|
||||
|
||||
const isPortfolioResult = result != null && 'reportId' in result;
|
||||
const isBrokerResult = result != null && 'cash' in result;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -85,12 +89,12 @@ export function ImportModal({ onClose, onDone }: Props) {
|
||||
{!result && (
|
||||
<div className="import-upload">
|
||||
<p className="import-upload__description">
|
||||
Выберите PDF/JSON выписки или JSON брокерского портфеля
|
||||
Выберите PDF/JSON выписки, JSON портфеля или XLSX-отчёт ВТБ Брокер
|
||||
</p>
|
||||
<input
|
||||
ref={fileRef}
|
||||
type="file"
|
||||
accept=".pdf,.json,application/pdf,application/json"
|
||||
accept=".pdf,.json,.xlsx,application/pdf,application/json,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
onChange={handleFileChange}
|
||||
className="import-upload__input"
|
||||
/>
|
||||
@@ -103,10 +107,31 @@ export function ImportModal({ onClose, onDone }: Props) {
|
||||
{result && (
|
||||
<div className="import-result">
|
||||
<div className="import-result__icon" aria-hidden="true">✓</div>
|
||||
<h3 className="import-result__title">{isPortfolioResult ? 'Импорт портфеля завершён' : 'Импорт завершён'}</h3>
|
||||
<h3 className="import-result__title">{isBrokerResult ? 'Импорт брокерского отчёта завершён' : isPortfolioResult ? 'Импорт портфеля завершён' : 'Импорт завершён'}</h3>
|
||||
<table className="import-result__stats">
|
||||
<tbody className="import-result__stats-body">
|
||||
{isPortfolioResult ? <>
|
||||
{isBrokerResult ? <>
|
||||
<tr className="import-result__stat-row">
|
||||
<td className="import-result__stat-cell import-result__stat-cell--label">Импортировано cash-операций</td>
|
||||
<td className="import-result__stat-cell import-result__stat-cell--value">{result.cash.imported}</td>
|
||||
</tr>
|
||||
<tr className="import-result__stat-row">
|
||||
<td className="import-result__stat-cell import-result__stat-cell--label">Дубликатов cash-операций</td>
|
||||
<td className="import-result__stat-cell import-result__stat-cell--value">{result.cash.duplicatesSkipped}</td>
|
||||
</tr>
|
||||
<tr className="import-result__stat-row">
|
||||
<td className="import-result__stat-cell import-result__stat-cell--label">Импортировано сделок</td>
|
||||
<td className="import-result__stat-cell import-result__stat-cell--value">{result.portfolio.importedTrades}</td>
|
||||
</tr>
|
||||
<tr className="import-result__stat-row">
|
||||
<td className="import-result__stat-cell import-result__stat-cell--label">Дубликатов сделок</td>
|
||||
<td className="import-result__stat-cell import-result__stat-cell--value">{result.portfolio.duplicateTrades}</td>
|
||||
</tr>
|
||||
<tr className="import-result__stat-row">
|
||||
<td className="import-result__stat-cell import-result__stat-cell--label">Позиций</td>
|
||||
<td className="import-result__stat-cell import-result__stat-cell--value">{result.portfolio.positions}</td>
|
||||
</tr>
|
||||
</> : isPortfolioResult ? <>
|
||||
<tr className="import-result__stat-row">
|
||||
<td className="import-result__stat-cell import-result__stat-cell--label">Импортировано сделок</td>
|
||||
<td className="import-result__stat-cell import-result__stat-cell--value">{result.importedTrades}</td>
|
||||
@@ -144,7 +169,7 @@ export function ImportModal({ onClose, onDone }: Props) {
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{!isPortfolioResult && result.isNewAccount && !aliasSaved && (
|
||||
{!isPortfolioResult && !isBrokerResult && result.isNewAccount && !aliasSaved && (
|
||||
<div className="import-result__alias">
|
||||
<label className="import-result__alias-label">Алиас для нового счёта</label>
|
||||
<div className="import-result__alias-row">
|
||||
|
||||
Reference in New Issue
Block a user