feat: import broker portfolio JSON

This commit is contained in:
2026-08-26 23:26:15 +03:00
parent 361a07d4da
commit c83a6d8d81
3 changed files with 55 additions and 28 deletions

View File

@@ -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": {

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

View File

@@ -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">