diff --git a/web/src/views/SimpleView.tsx b/web/src/views/SimpleView.tsx index 3bf8dfa..a0caab8 100644 --- a/web/src/views/SimpleView.tsx +++ b/web/src/views/SimpleView.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useReducer, useRef, useState } from 'react'; import type { FormEvent } from 'react'; import { Pencil, RefreshCw, Trash2 } from 'lucide-react'; import { useSearchParams } from 'react-router-dom'; @@ -17,23 +17,14 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../co import { Input } from '../components/ui/input'; import { PageHeader } from '../components/ui/page-header'; import { Select } from '../components/ui/select'; - -type SimpleSide = 'buy' | 'sell'; -type TriggerMode = 'dollar' | 'percent'; - -type SimpleSetupDraft = { - symbol: string; - side: SimpleSide; - sizingMode: 'quantity' | 'amount'; - quantity: string; - amountUsd: string; - currentMarketPrice: string; - dropMode: TriggerMode; - dropValue: string; - profitMode: TriggerMode; - profitValue: string; - notes: string; -}; +import { + DEFAULT_TRADE_PLANS_UI_STATE, + reduceTradePlansUiState, + type MarketPriceSource, + type SimpleSetupDraft, + type SimpleSide, + type TriggerMode, +} from './tradePlansState'; type SimpleHolding = { symbol: string; @@ -53,8 +44,6 @@ type SimpleRuntimeSnapshot = { type SimpleOperationalEvent = NonNullable['botState']['operationalEvents']>[number]; -type MarketPriceSource = 'live' | 'latest_close' | 'reference_price' | null; - const SIMPLE_AUTO_PROFILE_NAME = 'Simple Auto Profile'; const SIMPLE_AUTO_PROFILE_KEY = SIMPLE_AUTO_PROFILE_NAME.toLowerCase(); const SIMPLE_SYMBOL_DATALIST_ID = 'simple-supported-symbols'; @@ -72,20 +61,6 @@ const COMMON_SIMPLE_SYMBOLS = [ 'SOL/USD', ]; -const DEFAULT_DRAFT: SimpleSetupDraft = { - symbol: '', - side: 'buy', - sizingMode: 'quantity', - quantity: '', - amountUsd: '', - currentMarketPrice: '', - dropMode: 'percent', - dropValue: '', - profitMode: 'percent', - profitValue: '', - notes: '', -}; - function parsePositiveNumber(value: string): number | null { const trimmed = value.trim(); if (!trimmed) return null; @@ -620,22 +595,26 @@ export function SimpleView() { const [searchParams, setSearchParams] = useSearchParams(); const [profiles, setProfiles] = useState([]); const [savedSetups, setSavedSetups] = useState([]); - const [editingSetupId, setEditingSetupId] = useState(null); - const [draft, setDraft] = useState(DEFAULT_DRAFT); + const [uiState, dispatch] = useReducer(reduceTradePlansUiState, DEFAULT_TRADE_PLANS_UI_STATE); const [submitting, setSubmitting] = useState(false); const [loadingPrice, setLoadingPrice] = useState(false); - const [marketPriceSource, setMarketPriceSource] = useState(null); - const [copiedKey, setCopiedKey] = useState(null); - const [message, setMessage] = useState(null); - const [error, setError] = useState(null); - const [selectedHoldingTradeId, setSelectedHoldingTradeId] = useState(null); - const [focusedSetupId, setFocusedSetupId] = useState(null); const marketPriceRequestSymbolRef = useRef(''); const consumedPrefillKeyRef = useRef(''); const consumedSetupFocusKeyRef = useRef(''); const setupCardRefs = useRef>({}); const focusedSetupTimerRef = useRef(null); + const { + editingSetupId, + draft, + marketPriceSource, + copiedKey, + message, + error, + selectedHoldingTradeId, + focusedSetupId, + } = uiState; + const normalizedSymbol = draft.symbol.trim().toUpperCase(); const symbolState = botState?.symbols && typeof botState.symbols === 'object' ? botState.symbols : {}; const livePrice = normalizedSymbol ? Number(symbolState?.[normalizedSymbol]?.price || 0) : 0; @@ -728,7 +707,7 @@ export function SimpleView() { setSavedSetups(normalizeSimpleEntries(entryRows)); } catch (err: any) { if (cancelled) return; - setError(err?.message ?? 'Failed to load Simple setups'); + dispatch({ type: 'set-error', value: err?.message ?? 'Failed to load Simple setups' }); } } @@ -740,12 +719,10 @@ export function SimpleView() { useEffect(() => { if (!livePrice) return; - setMarketPriceSource('live'); - setDraft((prev) => ( - prev.currentMarketPrice === livePrice.toFixed(4) - ? prev - : { ...prev, currentMarketPrice: livePrice.toFixed(4) } - )); + dispatch({ type: 'set-market-price-source', value: 'live' }); + if (draft.currentMarketPrice !== livePrice.toFixed(4)) { + dispatch({ type: 'set-draft-field', key: 'currentMarketPrice', value: livePrice.toFixed(4) }); + } }, [livePrice]); useEffect(() => { @@ -777,24 +754,21 @@ export function SimpleView() { function updateDraft(key: K, value: SimpleSetupDraft[K]) { if (key === 'side' && value === 'buy') { - setSelectedHoldingTradeId(null); + dispatch({ type: 'set-selected-holding-trade-id', value: null }); } if (key === 'symbol' && draft.side === 'sell') { - setSelectedHoldingTradeId(null); + dispatch({ type: 'set-selected-holding-trade-id', value: null }); } - setDraft((prev) => ({ ...prev, [key]: value })); + dispatch({ type: 'set-draft-field', key, value }); } function applyHoldingToDraft(holding: SimpleHolding) { - setMarketPriceSource(null); - setSelectedHoldingTradeId(holding.tradeId || null); - setDraft((prev) => ({ - ...prev, - side: 'sell', + dispatch({ + type: 'apply-holding', + tradeId: holding.tradeId || null, symbol: holding.symbol, quantity: String(holding.size), - currentMarketPrice: '', - })); + }); } useEffect(() => { @@ -823,8 +797,8 @@ export function SimpleView() { if (selected) { applyHoldingToDraft(selected); - setMessage(`Loaded ${selected.symbol} from Portfolio. Configure the profit target and save the plan.`); - setError(null); + dispatch({ type: 'set-message', value: `Loaded ${selected.symbol} from Portfolio. Configure the profit target and save the plan.` }); + dispatch({ type: 'set-error', value: null }); } consumedPrefillKeyRef.current = prefillKey; setSearchParams({}, { replace: true }); @@ -842,14 +816,14 @@ export function SimpleView() { if (!targetEntry) return; - setFocusedSetupId(requestedSetupId); - setMessage(`Focused saved plan for ${targetEntry.symbol}.`); - setError(null); + dispatch({ type: 'set-focused-setup-id', value: requestedSetupId }); + dispatch({ type: 'set-message', value: `Focused saved plan for ${targetEntry.symbol}.` }); + dispatch({ type: 'set-error', value: null }); if (focusedSetupTimerRef.current !== null) { window.clearTimeout(focusedSetupTimerRef.current); } focusedSetupTimerRef.current = window.setTimeout(() => { - setFocusedSetupId((prev) => (prev === requestedSetupId ? null : prev)); + dispatch({ type: 'set-focused-setup-id', value: null }); focusedSetupTimerRef.current = null; }, 2200); window.requestAnimationFrame(() => { @@ -870,12 +844,12 @@ export function SimpleView() { try { await navigator.clipboard.writeText(value); const key = `${kind}:${value}`; - setCopiedKey(key); + dispatch({ type: 'set-copied-key', value: key }); window.setTimeout(() => { - setCopiedKey((prev) => (prev === key ? null : prev)); + dispatch({ type: 'set-copied-key', value: null }); }, 1200); } catch { - setError(`Failed to copy ${kind} ID`); + dispatch({ type: 'set-error', value: `Failed to copy ${kind} ID` }); } } @@ -898,7 +872,7 @@ export function SimpleView() { } function setMarketPriceValue(value: string, source: MarketPriceSource) { - setMarketPriceSource(source); + dispatch({ type: 'set-market-price-source', value: source }); updateDraft('currentMarketPrice', value); } @@ -908,8 +882,7 @@ export function SimpleView() { marketPriceRequestSymbolRef.current = requestSymbol; setLoadingPrice(true); if (!options?.silent) { - setError(null); - setMessage(null); + dispatch({ type: 'clear-feedback' }); } try { @@ -937,7 +910,7 @@ export function SimpleView() { } } catch (err: any) { if (!options?.silent && marketPriceRequestSymbolRef.current === requestSymbol) { - setError(err?.message ?? 'Failed to load market data'); + dispatch({ type: 'set-error', value: err?.message ?? 'Failed to load market data' }); } } finally { if (marketPriceRequestSymbolRef.current === requestSymbol) { @@ -949,8 +922,7 @@ export function SimpleView() { async function handleSubmit(e: FormEvent) { e.preventDefault(); setSubmitting(true); - setError(null); - setMessage(null); + dispatch({ type: 'clear-feedback' }); try { const existingEntry = editingSetupId @@ -965,37 +937,34 @@ export function SimpleView() { if (editingSetupId) { await updateManualEntry(editingSetupId, payload); - setMessage(`Updated ${normalizedSymbol} Simple setup.`); + dispatch({ type: 'set-message', value: `Updated ${normalizedSymbol} Simple setup.` }); } else { await createManualEntry({ ...payload, stock_instance_id: crypto.randomUUID(), }); - setMessage(`Saved ${normalizedSymbol} Simple setup.`); + dispatch({ type: 'set-message', value: `Saved ${normalizedSymbol} Simple setup.` }); } await refreshSetupList(); - setEditingSetupId(null); - setSelectedHoldingTradeId(null); - setMarketPriceSource(draft.currentMarketPrice ? marketPriceSource : null); - setDraft({ - ...DEFAULT_DRAFT, + dispatch({ + type: 'reset-form', currentMarketPrice: draft.currentMarketPrice, + marketPriceSource: draft.currentMarketPrice ? marketPriceSource : null, }); } catch (err: any) { - setError(err?.message ?? 'Failed to save Simple setup'); + dispatch({ type: 'set-error', value: err?.message ?? 'Failed to save Simple setup' }); } finally { setSubmitting(false); } } function handleEdit(entry: ManualEntryPayload) { - setEditingSetupId(String(entry.stock_instance_id || '')); - setSelectedHoldingTradeId(String(entry.linked_trade_id || '').trim() || null); - setMarketPriceSource(inferMarketPriceSourceFromEntry(entry)); - setDraft(buildDraftFromEntry(entry)); - setMessage(null); - setError(null); + dispatch({ type: 'set-editing-setup-id', value: String(entry.stock_instance_id || '') }); + dispatch({ type: 'set-selected-holding-trade-id', value: String(entry.linked_trade_id || '').trim() || null }); + dispatch({ type: 'set-market-price-source', value: inferMarketPriceSourceFromEntry(entry) }); + dispatch({ type: 'replace-draft', draft: buildDraftFromEntry(entry) }); + dispatch({ type: 'clear-feedback' }); } async function handleDelete(entryId: string) { @@ -1003,22 +972,18 @@ export function SimpleView() { try { await deleteManualEntry(entryId); if (editingSetupId === entryId) { - setEditingSetupId(null); - setSelectedHoldingTradeId(null); - setMarketPriceSource(null); - setDraft(DEFAULT_DRAFT); + dispatch({ type: 'reset-form', currentMarketPrice: '', marketPriceSource: null }); } await refreshSetupList(); } catch (err: any) { - setError(err?.message ?? 'Failed to delete Simple setup'); + dispatch({ type: 'set-error', value: err?.message ?? 'Failed to delete Simple setup' }); } } async function handleConvertToLongTerm(entry: ManualEntryPayload) { const entryId = String(entry.stock_instance_id || ''); if (!entryId) return; - setError(null); - setMessage(null); + dispatch({ type: 'clear-feedback' }); try { await updateSavedSetup(entryId, (current) => ({ ...current, @@ -1027,17 +992,16 @@ export function SimpleView() { status: 'simple_bought', active: true, })); - setMessage(`${String(entry.symbol || '').trim().toUpperCase()} is now treated as a long-term hold. Automated exit monitoring is paused.`); + dispatch({ type: 'set-message', value: `${String(entry.symbol || '').trim().toUpperCase()} is now treated as a long-term hold. Automated exit monitoring is paused.` }); } catch (err: any) { - setError(err?.message ?? 'Failed to convert setup to long-term mode'); + dispatch({ type: 'set-error', value: err?.message ?? 'Failed to convert setup to long-term mode' }); } } async function handleResumeExitManagement(entry: ManualEntryPayload) { const entryId = String(entry.stock_instance_id || ''); if (!entryId) return; - setError(null); - setMessage(null); + dispatch({ type: 'clear-feedback' }); try { await updateSavedSetup(entryId, (current) => ({ ...current, @@ -1046,9 +1010,9 @@ export function SimpleView() { status: 'simple_bought', active: true, })); - setMessage(`${String(entry.symbol || '').trim().toUpperCase()} is back under short-term exit management.`); + dispatch({ type: 'set-message', value: `${String(entry.symbol || '').trim().toUpperCase()} is back under short-term exit management.` }); } catch (err: any) { - setError(err?.message ?? 'Failed to resume short-term exit management'); + dispatch({ type: 'set-error', value: err?.message ?? 'Failed to resume short-term exit management' }); } } @@ -1072,15 +1036,11 @@ export function SimpleView() {