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 { SharePositionTable } from '@/widgets/share-positions-table';
|
||||
import { BondPositionTable } from '@/widgets/bond-positions-table';
|
||||
import { AddPositionForm } from '@/features/add-position';
|
||||
|
||||
export function PortfolioDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@ -13,18 +14,10 @@ export function PortfolioDetailPage() {
|
||||
|
||||
const { data: portfolio, isLoading, error } = usePortfolio(portfolioId);
|
||||
const { update, remove } = usePortfolioMutations();
|
||||
const {
|
||||
update: updatePosition,
|
||||
remove: removePosition,
|
||||
add: addPosition,
|
||||
} = usePositionMutations(portfolioId);
|
||||
const { update: updatePosition, remove: removePosition } = usePositionMutations(portfolioId);
|
||||
|
||||
const [editing, setEditing] = 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) {
|
||||
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 (
|
||||
<div>
|
||||
<div
|
||||
@ -167,108 +140,7 @@ export function PortfolioDetailPage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showAddForm && (
|
||||
<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>
|
||||
)}
|
||||
{showAddForm && <AddPositionForm portfolioId={portfolioId} />}
|
||||
|
||||
<SharePositionTable
|
||||
positions={portfolio.positions.filter((p) => p.type === 'share')}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user