codex/fsd-frontend-refactor #31

Merged
ksv741 merged 6 commits from codex/fsd-frontend-refactor into main 2026-06-20 23:29:09 +03:00
5 changed files with 151 additions and 131 deletions
Showing only changes of commit ad196164ee - Show all commits

View File

@ -0,0 +1,6 @@
import { usePositionMutations } from '@/entities/portfolio';
export function useAddPosition(portfolioId: number) {
const { add } = usePositionMutations(portfolioId);
return add;
}

View File

@ -0,0 +1 @@
export { AddPositionForm } from './ui/AddPositionForm';

View File

@ -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,
};
}

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

View File

@ -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')}