feat: import broker portfolio JSON
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@family-budget/frontend",
|
"name": "@family-budget/frontend",
|
||||||
"version": "0.11.2",
|
"version": "0.11.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
6
frontend/src/api/portfolio.ts
Normal file
6
frontend/src/api/portfolio.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import type { ImportPortfolioResponse, PortfolioFile } from '@family-budget/shared';
|
||||||
|
import { api } from './client';
|
||||||
|
|
||||||
|
export function importPortfolio(data: PortfolioFile): Promise<ImportPortfolioResponse> {
|
||||||
|
return api.post('/api/import/portfolio', data);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useRef } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
import type { ImportStatementResponse } from '@family-budget/shared';
|
import type { ImportPortfolioResponse, ImportStatementResponse, PortfolioFile } from '@family-budget/shared';
|
||||||
import { importStatement } from '../api/import';
|
import { importStatement } from '../api/import';
|
||||||
|
import { importPortfolio } from '../api/portfolio';
|
||||||
import { updateAccount } from '../api/accounts';
|
import { updateAccount } from '../api/accounts';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -9,7 +10,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ImportModal({ onClose, onDone }: Props) {
|
export function ImportModal({ onClose, onDone }: Props) {
|
||||||
const [result, setResult] = useState<ImportStatementResponse | null>(null);
|
const [result, setResult] = useState<ImportStatementResponse | ImportPortfolioResponse | null>(null);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [alias, setAlias] = useState('');
|
const [alias, setAlias] = useState('');
|
||||||
@@ -37,7 +38,10 @@ export function ImportModal({ onClose, onDone }: Props) {
|
|||||||
setResult(null);
|
setResult(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await importStatement(file);
|
const data = isJson ? JSON.parse(await file.text()) : null;
|
||||||
|
const resp = data?.schemaVersion === 'broker-portfolio-1.0'
|
||||||
|
? await importPortfolio(data as PortfolioFile)
|
||||||
|
: await importStatement(file);
|
||||||
setResult(resp);
|
setResult(resp);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const msg =
|
const msg =
|
||||||
@@ -49,7 +53,7 @@ export function ImportModal({ onClose, onDone }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveAlias = async () => {
|
const handleSaveAlias = async () => {
|
||||||
if (!result || !alias.trim()) return;
|
if (!result || 'reportId' in result || !alias.trim()) return;
|
||||||
try {
|
try {
|
||||||
await updateAccount(result.accountId, { alias: alias.trim() });
|
await updateAccount(result.accountId, { alias: alias.trim() });
|
||||||
setAliasSaved(true);
|
setAliasSaved(true);
|
||||||
@@ -58,6 +62,8 @@ export function ImportModal({ onClose, onDone }: Props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isPortfolioResult = result != null && 'reportId' in result;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="modal"
|
className="modal"
|
||||||
@@ -79,7 +85,7 @@ export function ImportModal({ onClose, onDone }: Props) {
|
|||||||
{!result && (
|
{!result && (
|
||||||
<div className="import-upload">
|
<div className="import-upload">
|
||||||
<p className="import-upload__description">
|
<p className="import-upload__description">
|
||||||
Выберите файл выписки (PDF или JSON, формат 1.0)
|
Выберите PDF/JSON выписки или JSON брокерского портфеля
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
ref={fileRef}
|
ref={fileRef}
|
||||||
@@ -97,33 +103,48 @@ export function ImportModal({ onClose, onDone }: Props) {
|
|||||||
{result && (
|
{result && (
|
||||||
<div className="import-result">
|
<div className="import-result">
|
||||||
<div className="import-result__icon" aria-hidden="true">✓</div>
|
<div className="import-result__icon" aria-hidden="true">✓</div>
|
||||||
<h3 className="import-result__title">Импорт завершён</h3>
|
<h3 className="import-result__title">{isPortfolioResult ? 'Импорт портфеля завершён' : 'Импорт завершён'}</h3>
|
||||||
<table className="import-result__stats">
|
<table className="import-result__stats">
|
||||||
<tbody className="import-result__stats-body">
|
<tbody className="import-result__stats-body">
|
||||||
<tr className="import-result__stat-row">
|
{isPortfolioResult ? <>
|
||||||
<td className="import-result__stat-cell import-result__stat-cell--label">Счёт</td>
|
<tr className="import-result__stat-row">
|
||||||
<td className="import-result__stat-cell import-result__stat-cell--value">{result.accountNumberMasked}</td>
|
<td className="import-result__stat-cell import-result__stat-cell--label">Импортировано сделок</td>
|
||||||
</tr>
|
<td className="import-result__stat-cell import-result__stat-cell--value">{result.importedTrades}</td>
|
||||||
<tr className="import-result__stat-row">
|
</tr>
|
||||||
<td className="import-result__stat-cell import-result__stat-cell--label">Новый счёт</td>
|
<tr className="import-result__stat-row">
|
||||||
<td className="import-result__stat-cell import-result__stat-cell--value">{result.isNewAccount ? 'Да' : 'Нет'}</td>
|
<td className="import-result__stat-cell import-result__stat-cell--label">Дубликатов сделок</td>
|
||||||
</tr>
|
<td className="import-result__stat-cell import-result__stat-cell--value">{result.duplicateTrades}</td>
|
||||||
<tr className="import-result__stat-row">
|
</tr>
|
||||||
<td className="import-result__stat-cell import-result__stat-cell--label">Импортировано</td>
|
<tr className="import-result__stat-row">
|
||||||
<td className="import-result__stat-cell import-result__stat-cell--value">{result.imported}</td>
|
<td className="import-result__stat-cell import-result__stat-cell--label">Позиций</td>
|
||||||
</tr>
|
<td className="import-result__stat-cell import-result__stat-cell--value">{result.positions}</td>
|
||||||
<tr className="import-result__stat-row">
|
</tr>
|
||||||
<td className="import-result__stat-cell import-result__stat-cell--label">Дубликатов пропущено</td>
|
</> : <>
|
||||||
<td className="import-result__stat-cell import-result__stat-cell--value">{result.duplicatesSkipped}</td>
|
<tr className="import-result__stat-row">
|
||||||
</tr>
|
<td className="import-result__stat-cell import-result__stat-cell--label">Счёт</td>
|
||||||
<tr className="import-result__stat-row">
|
<td className="import-result__stat-cell import-result__stat-cell--value">{result.accountNumberMasked}</td>
|
||||||
<td className="import-result__stat-cell import-result__stat-cell--label">Всего в файле</td>
|
</tr>
|
||||||
<td className="import-result__stat-cell import-result__stat-cell--value">{result.totalInFile}</td>
|
<tr className="import-result__stat-row">
|
||||||
</tr>
|
<td className="import-result__stat-cell import-result__stat-cell--label">Новый счёт</td>
|
||||||
|
<td className="import-result__stat-cell import-result__stat-cell--value">{result.isNewAccount ? 'Да' : 'Нет'}</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.imported}</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.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.totalInFile}</td>
|
||||||
|
</tr>
|
||||||
|
</>}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{result.isNewAccount && !aliasSaved && (
|
{!isPortfolioResult && result.isNewAccount && !aliasSaved && (
|
||||||
<div className="import-result__alias">
|
<div className="import-result__alias">
|
||||||
<label className="import-result__alias-label">Алиас для нового счёта</label>
|
<label className="import-result__alias-label">Алиас для нового счёта</label>
|
||||||
<div className="import-result__alias-row">
|
<div className="import-result__alias-row">
|
||||||
|
|||||||
Reference in New Issue
Block a user