refactor: extract AddPositionForm to features layer
This commit is contained in:
parent
d1f9128441
commit
ad196164ee
@ -0,0 +1,6 @@
|
|||||||
|
import { usePositionMutations } from '@/entities/portfolio';
|
||||||
|
|
||||||
|
export function useAddPosition(portfolioId: number) {
|
||||||
|
const { add } = usePositionMutations(portfolioId);
|
||||||
|
return add;
|
||||||
|
}
|
||||||
1
apps/frontend/src/features/add-position/index.ts
Normal file
1
apps/frontend/src/features/add-position/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { AddPositionForm } from './ui/AddPositionForm';
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export function useAddPositionForm() {
|
||||||
|
const [showAddForm, setShowAddForm] = useState(false);
|
||||||
|
const [newSecid, setNewSecid] = useState('');
|
||||||
|
const [newQty, setNewQty] = useState('1');
|
||||||
|
const [newPrice, setNewPrice] = useState('');
|
||||||
|
const [newDate, setNewDate] = useState(new Date().toISOString().split('T')[0]);
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
setNewSecid('');
|
||||||
|
setNewQty('1');
|
||||||
|
setNewPrice('');
|
||||||
|
setNewDate(new Date().toISOString().split('T')[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
showAddForm,
|
||||||
|
setShowAddForm,
|
||||||
|
newSecid,
|
||||||
|
setNewSecid,
|
||||||
|
newQty,
|
||||||
|
setNewQty,
|
||||||
|
newPrice,
|
||||||
|
setNewPrice,
|
||||||
|
newDate,
|
||||||
|
setNewDate,
|
||||||
|
reset,
|
||||||
|
};
|
||||||
|
}
|
||||||
111
apps/frontend/src/features/add-position/ui/AddPositionForm.tsx
Normal file
111
apps/frontend/src/features/add-position/ui/AddPositionForm.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { useAddPosition } from '../api/useAddPosition';
|
||||||
|
import { useAddPositionForm } from '../model/useAddPositionForm';
|
||||||
|
|
||||||
|
const inputStyle: React.CSSProperties = {
|
||||||
|
padding: '8px 12px',
|
||||||
|
border: '1px solid #e0e0e0',
|
||||||
|
borderRadius: 'var(--border-radius)',
|
||||||
|
fontSize: 14,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AddPositionForm({ portfolioId }: { portfolioId: number }) {
|
||||||
|
const addPosition = useAddPosition(portfolioId);
|
||||||
|
const form = useAddPositionForm();
|
||||||
|
|
||||||
|
function handleAddPosition() {
|
||||||
|
if (!form.newSecid.trim() || !parseInt(form.newQty, 10)) return;
|
||||||
|
addPosition.mutate(
|
||||||
|
{
|
||||||
|
secid: form.newSecid.trim().toUpperCase(),
|
||||||
|
quantity: parseInt(form.newQty, 10),
|
||||||
|
buyPrice: form.newPrice ? parseFloat(form.newPrice) : undefined,
|
||||||
|
buyDate: form.newDate || undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
form.setShowAddForm(false);
|
||||||
|
form.reset();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 12,
|
||||||
|
padding: 16,
|
||||||
|
background: 'var(--color-surface)',
|
||||||
|
border: '1px solid #e0e0e0',
|
||||||
|
borderRadius: 'var(--border-radius)',
|
||||||
|
display: 'flex',
|
||||||
|
gap: 12,
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', fontSize: 12, fontWeight: 600, marginBottom: 4 }}>
|
||||||
|
Тикер
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
value={form.newSecid}
|
||||||
|
onChange={(e) => form.setNewSecid(e.target.value)}
|
||||||
|
placeholder="SBER"
|
||||||
|
style={{ ...inputStyle, width: 120 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', fontSize: 12, fontWeight: 600, marginBottom: 4 }}>
|
||||||
|
Количество
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={form.newQty}
|
||||||
|
onChange={(e) => form.setNewQty(e.target.value)}
|
||||||
|
style={{ ...inputStyle, width: 100 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', fontSize: 12, fontWeight: 600, marginBottom: 4 }}>
|
||||||
|
Цена покупки
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={form.newPrice}
|
||||||
|
onChange={(e) => form.setNewPrice(e.target.value)}
|
||||||
|
placeholder="0.00"
|
||||||
|
style={{ ...inputStyle, width: 120 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', fontSize: 12, fontWeight: 600, marginBottom: 4 }}>
|
||||||
|
Дата покупки
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={form.newDate}
|
||||||
|
onChange={(e) => form.setNewDate(e.target.value)}
|
||||||
|
style={{ ...inputStyle, width: 150 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleAddPosition}
|
||||||
|
disabled={addPosition.isPending}
|
||||||
|
style={{
|
||||||
|
padding: '8px 16px',
|
||||||
|
background: 'var(--color-primary)',
|
||||||
|
color: '#fff',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 'var(--border-radius)',
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 600,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Добавить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ import { PortfolioSummary } from '@/widgets/portfolio-summary';
|
|||||||
import { AnalyticsSummary } from '@/widgets/portfolio-analytics';
|
import { AnalyticsSummary } from '@/widgets/portfolio-analytics';
|
||||||
import { SharePositionTable } from '@/widgets/share-positions-table';
|
import { SharePositionTable } from '@/widgets/share-positions-table';
|
||||||
import { BondPositionTable } from '@/widgets/bond-positions-table';
|
import { BondPositionTable } from '@/widgets/bond-positions-table';
|
||||||
|
import { AddPositionForm } from '@/features/add-position';
|
||||||
|
|
||||||
export function PortfolioDetailPage() {
|
export function PortfolioDetailPage() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
@ -13,18 +14,10 @@ export function PortfolioDetailPage() {
|
|||||||
|
|
||||||
const { data: portfolio, isLoading, error } = usePortfolio(portfolioId);
|
const { data: portfolio, isLoading, error } = usePortfolio(portfolioId);
|
||||||
const { update, remove } = usePortfolioMutations();
|
const { update, remove } = usePortfolioMutations();
|
||||||
const {
|
const { update: updatePosition, remove: removePosition } = usePositionMutations(portfolioId);
|
||||||
update: updatePosition,
|
|
||||||
remove: removePosition,
|
|
||||||
add: addPosition,
|
|
||||||
} = usePositionMutations(portfolioId);
|
|
||||||
|
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [showAddForm, setShowAddForm] = useState(false);
|
const [showAddForm, setShowAddForm] = useState(false);
|
||||||
const [newSecid, setNewSecid] = useState('');
|
|
||||||
const [newQty, setNewQty] = useState('1');
|
|
||||||
const [newPrice, setNewPrice] = useState('');
|
|
||||||
const [newDate, setNewDate] = useState(new Date().toISOString().split('T')[0]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@ -48,26 +41,6 @@ export function PortfolioDetailPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAddPosition() {
|
|
||||||
if (!newSecid.trim() || !parseInt(newQty, 10)) return;
|
|
||||||
addPosition.mutate(
|
|
||||||
{
|
|
||||||
secid: newSecid.trim().toUpperCase(),
|
|
||||||
quantity: parseInt(newQty, 10),
|
|
||||||
buyPrice: newPrice ? parseFloat(newPrice) : undefined,
|
|
||||||
buyDate: newDate || undefined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
setShowAddForm(false);
|
|
||||||
setNewSecid('');
|
|
||||||
setNewQty('1');
|
|
||||||
setNewPrice('');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
@ -167,108 +140,7 @@ export function PortfolioDetailPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showAddForm && (
|
{showAddForm && <AddPositionForm portfolioId={portfolioId} />}
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
marginTop: 12,
|
|
||||||
padding: 16,
|
|
||||||
background: 'var(--color-surface)',
|
|
||||||
border: '1px solid #e0e0e0',
|
|
||||||
borderRadius: 'var(--border-radius)',
|
|
||||||
display: 'flex',
|
|
||||||
gap: 12,
|
|
||||||
alignItems: 'flex-end',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<label style={{ display: 'block', fontSize: 12, fontWeight: 600, marginBottom: 4 }}>
|
|
||||||
Тикер
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
value={newSecid}
|
|
||||||
onChange={(e) => setNewSecid(e.target.value)}
|
|
||||||
placeholder="SBER"
|
|
||||||
style={{
|
|
||||||
padding: '8px 12px',
|
|
||||||
border: '1px solid #e0e0e0',
|
|
||||||
borderRadius: 'var(--border-radius)',
|
|
||||||
fontSize: 14,
|
|
||||||
width: 120,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label style={{ display: 'block', fontSize: 12, fontWeight: 600, marginBottom: 4 }}>
|
|
||||||
Количество
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
value={newQty}
|
|
||||||
onChange={(e) => setNewQty(e.target.value)}
|
|
||||||
style={{
|
|
||||||
padding: '8px 12px',
|
|
||||||
border: '1px solid #e0e0e0',
|
|
||||||
borderRadius: 'var(--border-radius)',
|
|
||||||
fontSize: 14,
|
|
||||||
width: 100,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label style={{ display: 'block', fontSize: 12, fontWeight: 600, marginBottom: 4 }}>
|
|
||||||
Цена покупки
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
value={newPrice}
|
|
||||||
onChange={(e) => setNewPrice(e.target.value)}
|
|
||||||
placeholder="0.00"
|
|
||||||
style={{
|
|
||||||
padding: '8px 12px',
|
|
||||||
border: '1px solid #e0e0e0',
|
|
||||||
borderRadius: 'var(--border-radius)',
|
|
||||||
fontSize: 14,
|
|
||||||
width: 120,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label style={{ display: 'block', fontSize: 12, fontWeight: 600, marginBottom: 4 }}>
|
|
||||||
Дата покупки
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={newDate}
|
|
||||||
onChange={(e) => setNewDate(e.target.value)}
|
|
||||||
style={{
|
|
||||||
padding: '8px 12px',
|
|
||||||
border: '1px solid #e0e0e0',
|
|
||||||
borderRadius: 'var(--border-radius)',
|
|
||||||
fontSize: 14,
|
|
||||||
width: 150,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={handleAddPosition}
|
|
||||||
disabled={addPosition.isPending}
|
|
||||||
style={{
|
|
||||||
padding: '8px 16px',
|
|
||||||
background: 'var(--color-primary)',
|
|
||||||
color: '#fff',
|
|
||||||
border: 'none',
|
|
||||||
borderRadius: 'var(--border-radius)',
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: 600,
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Добавить
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<SharePositionTable
|
<SharePositionTable
|
||||||
positions={portfolio.positions.filter((p) => p.type === 'share')}
|
positions={portfolio.positions.filter((p) => p.type === 'share')}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user