feat: move manual entries behind backend api

This commit is contained in:
Saravana Achu Mac 2026-04-04 16:09:21 -07:00
parent 44f3171783
commit ebaabaed47
7 changed files with 437 additions and 231 deletions

View File

@ -23,6 +23,11 @@ import {
saveCurrentUserProfile,
saveTradeProfileForUser,
} from './profileRepository.js';
import {
deleteManualEntryForUser,
listManualEntriesForUser,
saveManualEntryForUser
} from './manualEntryRepository.js';
import { mergeOrderSnapshots, mergePositionSnapshots } from './stateMerge.js';
import { OperationalEvent } from '../domain/operationalEvents.js';
import { runBacktest } from '../backtest/index.js';
@ -1799,6 +1804,69 @@ export class ApiServer {
}
});
this.app.get('/api/manual-entries', this.requireAuth, async (req, res) => {
const authUserId = (req as AuthenticatedRequest).authUserId;
if (!authUserId) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
try {
const entries = await listManualEntriesForUser(authUserId, supabaseService);
res.json({ entries });
} catch (error: any) {
res.status(500).json({ error: `Failed to load manual entries: ${error.message}` });
}
});
this.app.post('/api/manual-entries', this.requireAuth, async (req, res) => {
const authUserId = (req as AuthenticatedRequest).authUserId;
if (!authUserId) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
try {
const entry = await saveManualEntryForUser(authUserId, req.body || {}, supabaseService);
res.status(201).json({ entry });
} catch (error: any) {
res.status(400).json({ error: `Failed to save manual entry: ${error.message}` });
}
});
this.app.put('/api/manual-entries/:id', this.requireAuth, async (req, res) => {
const authUserId = (req as AuthenticatedRequest).authUserId;
if (!authUserId) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
try {
const entry = await saveManualEntryForUser(authUserId, {
...(req.body || {}),
stock_instance_id: String(req.params.id || '').trim()
}, supabaseService);
res.json({ entry });
} catch (error: any) {
res.status(400).json({ error: `Failed to update manual entry: ${error.message}` });
}
});
this.app.delete('/api/manual-entries/:id', this.requireAuth, async (req, res) => {
const authUserId = (req as AuthenticatedRequest).authUserId;
if (!authUserId) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
try {
await deleteManualEntryForUser(authUserId, String(req.params.id || '').trim(), supabaseService);
res.json({ success: true });
} catch (error: any) {
res.status(400).json({ error: `Failed to delete manual entry: ${error.message}` });
}
});
this.app.get('/api/admin/config/dynamic', this.requireAuth, this.requireAdmin, async (_req, res) => {
try {
const items = await listDynamicConfigEntries(supabaseService);

View File

@ -0,0 +1,188 @@
import { randomUUID } from 'node:crypto';
import logger from '../utils/logger.js';
import type { supabaseService } from './SupabaseService.js';
type LegacySupabaseService = typeof supabaseService;
export interface ManualEntryRecord {
stock_instance_id: string;
symbol: string;
active: boolean;
user_id: string;
buy_price?: number | null;
sell_price?: number | null;
buy_time?: string | null;
sell_time?: string | null;
quantity?: number | null;
filled_quantity?: number | null;
notes?: string | null;
status: string;
is_crypto: boolean;
is_real_trade: boolean;
label?: string | null;
entry_price?: number | null;
gain_threshold_for_sell?: number | null;
drop_threshold_for_buy?: number | null;
}
function getClient(legacyService?: LegacySupabaseService) {
const client = legacyService?.getClient?.();
if (!client) {
throw new Error('Manual entry store is not configured');
}
return client;
}
function normalizeNullableNumber(value: unknown): number | null | undefined {
if (value === undefined) return undefined;
if (value === null || value === '') return null;
const num = Number(value);
return Number.isFinite(num) ? num : null;
}
function normalizeNullableString(value: unknown): string | null | undefined {
if (value === undefined) return undefined;
if (value === null) return null;
const text = String(value).trim();
return text ? text : null;
}
function normalizeEntry(userId: string, input: Partial<ManualEntryRecord>, existing?: ManualEntryRecord | null): ManualEntryRecord {
return {
stock_instance_id: String(input.stock_instance_id || existing?.stock_instance_id || randomUUID()),
symbol: String(input.symbol || existing?.symbol || '').trim(),
active: Boolean(input.active ?? existing?.active ?? true),
user_id: userId,
buy_price: normalizeNullableNumber(input.buy_price ?? existing?.buy_price),
sell_price: normalizeNullableNumber(input.sell_price ?? existing?.sell_price),
buy_time: normalizeNullableString(input.buy_time ?? existing?.buy_time),
sell_time: normalizeNullableString(input.sell_time ?? existing?.sell_time),
quantity: normalizeNullableNumber(input.quantity ?? existing?.quantity),
filled_quantity: normalizeNullableNumber(input.filled_quantity ?? existing?.filled_quantity),
notes: normalizeNullableString(input.notes ?? existing?.notes),
status: String(input.status || existing?.status || 'active'),
is_crypto: Boolean(input.is_crypto ?? existing?.is_crypto ?? false),
is_real_trade: Boolean(input.is_real_trade ?? existing?.is_real_trade ?? false),
label: normalizeNullableString(input.label ?? existing?.label),
entry_price: normalizeNullableNumber(input.entry_price ?? existing?.entry_price),
gain_threshold_for_sell: normalizeNullableNumber(input.gain_threshold_for_sell ?? existing?.gain_threshold_for_sell),
drop_threshold_for_buy: normalizeNullableNumber(input.drop_threshold_for_buy ?? existing?.drop_threshold_for_buy),
};
}
async function insertHistoryRecord(userId: string, entry: ManualEntryRecord, reason: 'Manual Log' | 'Manual Close', legacyService?: LegacySupabaseService): Promise<void> {
const client = getClient(legacyService);
const entryPrice = Number(entry.buy_price || 0);
const exitPrice = Number(entry.sell_price || 0);
const size = Number(entry.quantity || 0);
if (!(entryPrice > 0) || !(exitPrice > 0) || !(size > 0)) {
return;
}
const pnl = (exitPrice - entryPrice) * size;
const pnlPercent = entryPrice > 0 ? ((exitPrice - entryPrice) / entryPrice) * 100 : 0;
const { error } = await client
.from('trade_history')
.insert([{
user_id: userId,
symbol: entry.symbol,
side: 'BUY',
entry_price: entryPrice,
exit_price: exitPrice,
size,
pnl,
pnl_percent: pnlPercent,
source: 'MANUAL',
reason,
timestamp: Date.now()
}]);
if (error) {
logger.warn(`[ManualEntries] Failed to insert history row: ${error.message}`);
}
}
export async function listManualEntriesForUser(userId: string, legacyService?: LegacySupabaseService): Promise<ManualEntryRecord[]> {
const client = getClient(legacyService);
const { data, error } = await client
.from('stocks')
.select('*')
.eq('user_id', userId)
.order('created_at', { ascending: false });
if (error) {
throw new Error(error.message);
}
return Array.isArray(data) ? data as ManualEntryRecord[] : [];
}
export async function saveManualEntryForUser(
userId: string,
input: Partial<ManualEntryRecord>,
legacyService?: LegacySupabaseService
): Promise<ManualEntryRecord> {
const client = getClient(legacyService);
const entryId = String(input.stock_instance_id || '').trim();
let existing: ManualEntryRecord | null = null;
if (entryId) {
const { data, error } = await client
.from('stocks')
.select('*')
.eq('stock_instance_id', entryId)
.eq('user_id', userId)
.maybeSingle();
if (error) {
throw new Error(error.message);
}
existing = data as ManualEntryRecord | null;
}
const normalized = normalizeEntry(userId, input, existing);
if (!normalized.symbol) {
throw new Error('Symbol is required');
}
if (existing) {
const { error } = await client
.from('stocks')
.update(normalized)
.eq('stock_instance_id', normalized.stock_instance_id)
.eq('user_id', userId);
if (error) {
throw new Error(error.message);
}
if (normalized.sell_price && !existing.sell_price && normalized.buy_price) {
await insertHistoryRecord(userId, normalized, 'Manual Close', legacyService);
}
return normalized;
}
const { error } = await client
.from('stocks')
.insert([normalized]);
if (error) {
throw new Error(error.message);
}
if (normalized.sell_price && normalized.buy_price) {
await insertHistoryRecord(userId, normalized, 'Manual Log', legacyService);
}
return normalized;
}
export async function deleteManualEntryForUser(userId: string, entryId: string, legacyService?: LegacySupabaseService): Promise<void> {
const client = getClient(legacyService);
const { error } = await client
.from('stocks')
.delete()
.eq('stock_instance_id', entryId)
.eq('user_id', userId);
if (error) {
throw new Error(error.message);
}
}

View File

@ -3,23 +3,16 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { EntryForm } from './EntryForm';
import { tableNameStocks } from '../lib/const';
const {
getSessionMock,
stocksInsertMock,
stocksUpdateEqMock,
stocksUpdateMock,
historyInsertMock,
fromMock,
createManualEntryMock,
updateManualEntryMock,
authMock
} = vi.hoisted(() => ({
getSessionMock: vi.fn(),
stocksInsertMock: vi.fn(),
stocksUpdateEqMock: vi.fn(),
stocksUpdateMock: vi.fn(),
historyInsertMock: vi.fn(),
fromMock: vi.fn(),
createManualEntryMock: vi.fn(),
updateManualEntryMock: vi.fn(),
authMock: { user: { id: 'user-1' } as any }
}));
@ -31,48 +24,29 @@ vi.mock('../lib/supabaseClient', () => ({
supabase: {
auth: {
getSession: getSessionMock
},
from: fromMock
}
}
}));
vi.mock('../lib/manualEntriesApi', () => ({
createManualEntry: createManualEntryMock,
updateManualEntry: updateManualEntryMock
}));
describe('EntryForm DOM flow', () => {
const alertMock = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
stocksInsertMock.mockReset();
stocksUpdateEqMock.mockReset();
stocksUpdateMock.mockReset();
historyInsertMock.mockReset();
createManualEntryMock.mockReset();
updateManualEntryMock.mockReset();
getSessionMock.mockReset();
fromMock.mockReset();
authMock.user = { id: 'user-1' };
stocksInsertMock.mockResolvedValue({ error: null });
stocksUpdateEqMock.mockResolvedValue({ error: null });
stocksUpdateMock.mockReturnValue({ eq: stocksUpdateEqMock });
historyInsertMock.mockResolvedValue({ error: null });
createManualEntryMock.mockResolvedValue({});
updateManualEntryMock.mockResolvedValue({});
getSessionMock.mockResolvedValue({ data: { session: null } });
fromMock.mockImplementation((table: string) => {
if (table === tableNameStocks) {
return {
insert: stocksInsertMock,
update: stocksUpdateMock
};
}
if (table === 'trade_history') {
return {
insert: historyInsertMock
};
}
return {
insert: vi.fn().mockResolvedValue({ error: null }),
update: vi.fn(() => ({ eq: vi.fn().mockResolvedValue({ error: null }) }))
};
});
vi.stubGlobal('fetch', vi.fn());
vi.stubGlobal('confirm', vi.fn(() => true));
vi.stubGlobal('alert', alertMock);
@ -91,7 +65,7 @@ describe('EntryForm DOM flow', () => {
await user.click(screen.getByRole('button', { name: 'Add' }));
await waitFor(() => {
expect(stocksInsertMock).toHaveBeenCalled();
expect(createManualEntryMock).toHaveBeenCalled();
expect(onSuccess).toHaveBeenCalled();
});
});
@ -127,13 +101,17 @@ describe('EntryForm DOM flow', () => {
await user.click(screen.getByRole('button', { name: 'Add' }));
await waitFor(() => {
expect(historyInsertMock).toHaveBeenCalledTimes(1);
expect(createManualEntryMock).toHaveBeenCalledTimes(1);
});
expect(historyInsertMock.mock.calls[0][0][0].reason).toBe('Manual Log');
expect(createManualEntryMock.mock.calls[0][0]).toEqual(expect.objectContaining({
symbol: 'BTC/USD',
buy_price: 100,
sell_price: 110
}));
});
it('handles database error by alerting user', async () => {
stocksInsertMock.mockResolvedValue({ error: { message: 'DB Error' } });
createManualEntryMock.mockRejectedValueOnce(new Error('DB Error'));
const user = userEvent.setup();
render(<EntryForm onSuccess={vi.fn()} />);
@ -152,6 +130,6 @@ describe('EntryForm DOM flow', () => {
await user.type(screen.getByPlaceholderText('BTC/USD'), 'BTC/USD');
await user.click(screen.getByRole('button', { name: 'Add' }));
expect(stocksInsertMock).not.toHaveBeenCalled();
expect(createManualEntryMock).not.toHaveBeenCalled();
});
});

View File

@ -2,8 +2,8 @@ import { useState, useEffect } from 'react';
import type { FormEvent } from 'react';
import { supabase } from '../lib/supabaseClient';
import { useAuth } from '../components/AuthContext';
import { tableNameStocks } from '../lib/const';
import { tradingRuntime } from '../lib/runtime';
import { createManualEntry, updateManualEntry } from '../lib/manualEntriesApi';
interface EntryFormProps {
onSuccess: () => void;
@ -143,88 +143,13 @@ export function EntryForm({ onSuccess, initialData }: EntryFormProps) {
throw new Error(`Execution Failed: ${resData.error}`);
}
// If success, we continue to save to DB as usual for journaling
alert(`✅ Trade Executed! Order ID: ${resData.orderId}`);
}
if (initialData?.stock_instance_id) {
// Update
const { error } = await supabase
.from(tableNameStocks)
.update(payload)
.eq('stock_instance_id', initialData.stock_instance_id);
if (error) throw error;
// --- NEW: Log to Trade History if Closing ---
// If we are adding a sell_price to an open position, we consider it "Closing"
if (payload.sell_price && !initialData.sell_price && payload.buy_price) {
const entryPrice = payload.buy_price;
const exitPrice = payload.sell_price;
const size = payload.quantity || 0;
const pnl = (exitPrice - entryPrice) * size;
const pnlPercent = ((exitPrice - entryPrice) / entryPrice) * 100;
// Insert into trade_history
const { error: historyError } = await supabase
.from('trade_history')
.insert([{
user_id: user.id,
symbol: payload.symbol,
side: 'BUY',
entry_price: entryPrice,
exit_price: exitPrice,
size: size,
pnl: pnl,
pnl_percent: pnlPercent,
source: 'MANUAL',
reason: 'Manual Close',
timestamp: Date.now()
}]);
if (historyError) {
console.error("Failed to log manual trade to history:", historyError.message);
}
}
await updateManualEntry(initialData.stock_instance_id, payload);
} else {
// Insert
// Remove ID to let DB generate it
const { error } = await supabase
.from(tableNameStocks)
.insert([{ ...payload, stock_instance_id: crypto.randomUUID() }]);
if (error) throw error;
// --- NEW: Log to Trade History if Creating a Closed Entry (Journaling past trade) ---
if (payload.sell_price && payload.buy_price) {
const entryPrice = payload.buy_price;
const exitPrice = payload.sell_price;
const size = payload.quantity || 0;
const pnl = (exitPrice - entryPrice) * size;
const pnlPercent = ((exitPrice - entryPrice) / entryPrice) * 100;
// Insert into trade_history
const { error: historyError } = await supabase
.from('trade_history')
.insert([{
user_id: user.id,
symbol: payload.symbol,
side: 'BUY',
entry_price: entryPrice,
exit_price: exitPrice,
size: size,
pnl: pnl,
pnl_percent: pnlPercent,
source: 'MANUAL',
reason: 'Manual Log',
timestamp: Date.now()
}]);
if (historyError) {
console.error("Failed to log manual trade to history:", historyError.message);
}
}
await createManualEntry({ ...payload, stock_instance_id: crypto.randomUUID() });
}
onSuccess();

View File

@ -0,0 +1,77 @@
import { supabase } from './supabaseClient';
import { tradingRuntime } from './runtime';
export interface ManualEntryPayload {
stock_instance_id?: string;
symbol: string;
active: boolean;
user_id?: string;
buy_price?: number | null;
sell_price?: number | null;
buy_time?: string | null;
sell_time?: string | null;
quantity?: number | null;
filled_quantity?: number | null;
notes?: string | null;
status: string;
is_crypto: boolean;
is_real_trade: boolean;
label?: string | null;
entry_price?: number | null;
gain_threshold_for_sell?: number | null;
drop_threshold_for_buy?: number | null;
}
async function getAccessToken(): Promise<string> {
const { data: sessionData } = await supabase.auth.getSession();
const accessToken = sessionData.session?.access_token;
if (!accessToken) {
throw new Error('Not authenticated');
}
return accessToken;
}
async function apiRequest<T>(path: string, init?: RequestInit): Promise<T> {
const accessToken = await getAccessToken();
const response = await fetch(`${tradingRuntime.tradingApiUrl}${path}`, {
...init,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
...(init?.headers || {}),
},
});
const body = await response.json().catch(() => ({} as any));
if (!response.ok) {
throw new Error(body?.error || `Request failed (${response.status})`);
}
return body as T;
}
export async function fetchManualEntries(): Promise<ManualEntryPayload[]> {
const response = await apiRequest<{ entries: ManualEntryPayload[] }>('/api/manual-entries');
return Array.isArray(response.entries) ? response.entries : [];
}
export async function createManualEntry(payload: ManualEntryPayload): Promise<ManualEntryPayload> {
const response = await apiRequest<{ entry: ManualEntryPayload }>('/api/manual-entries', {
method: 'POST',
body: JSON.stringify(payload),
});
return response.entry;
}
export async function updateManualEntry(id: string, payload: ManualEntryPayload): Promise<ManualEntryPayload> {
const response = await apiRequest<{ entry: ManualEntryPayload }>(`/api/manual-entries/${id}`, {
method: 'PUT',
body: JSON.stringify(payload),
});
return response.entry;
}
export async function deleteManualEntry(id: string): Promise<void> {
await apiRequest<{ success: boolean }>(`/api/manual-entries/${id}`, {
method: 'DELETE',
});
}

View File

@ -1,32 +1,35 @@
// @vitest-environment jsdom
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { render, screen, waitFor, fireEvent, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { EntriesTab } from './EntriesTab';
import { tableNameStocks } from '../lib/const';
const {
authState,
fromMock,
confirmMock
} = vi.hoisted(() => ({
authState: {
user: { id: 'user-1', email: 'test@demo.test' } as any,
profile: { role: 'user' } as any
},
fromMock: vi.fn(),
confirmMock: vi.fn()
}));
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { render, screen, waitFor, fireEvent, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { EntriesTab } from './EntriesTab';
const {
authState,
confirmMock,
fetchManualEntriesMock,
deleteManualEntryMock,
createManualEntryMock
} = vi.hoisted(() => ({
authState: {
user: { id: 'user-1', email: 'test@demo.test' } as any,
profile: { role: 'user' } as any
},
confirmMock: vi.fn(),
fetchManualEntriesMock: vi.fn(),
deleteManualEntryMock: vi.fn(),
createManualEntryMock: vi.fn()
}));
vi.mock('../components/AuthContext', () => ({
useAuth: () => authState
}));
vi.mock('../lib/supabaseClient', () => ({
supabase: {
from: fromMock
}
}));
vi.mock('../lib/manualEntriesApi', () => ({
fetchManualEntries: fetchManualEntriesMock,
deleteManualEntry: deleteManualEntryMock,
createManualEntry: createManualEntryMock
}));
vi.mock('../components/EntryForm', () => ({
EntryForm: ({ onSuccess, initialData }: any) => (
@ -37,21 +40,7 @@ vi.mock('../components/EntryForm', () => ({
)
}));
const createMockQuery = (data: any, error: any = null) => {
const chain: any = {
select: vi.fn(),
eq: vi.fn(),
order: vi.fn(),
then: vi.fn()
};
chain.select.mockReturnValue(chain);
chain.eq.mockReturnValue(chain);
chain.order.mockReturnValue(chain);
chain.then.mockImplementation((cb: any) => Promise.resolve({ data, error }).then(cb));
return chain;
};
describe('EntriesTab master suite', () => {
describe('EntriesTab master suite', () => {
const mockEntry = {
stock_instance_id: 'entry-1',
symbol: 'BTC/USDT',
@ -75,26 +64,16 @@ describe('EntriesTab master suite', () => {
settings: { executionMode: 'Paper' }
};
beforeEach(() => {
vi.clearAllMocks();
authState.user = { id: 'user-1', email: 'test@demo.test' };
authState.profile = { role: 'user' };
vi.stubGlobal('confirm', confirmMock);
vi.stubGlobal('crypto', { randomUUID: () => 'uuid-123' });
fromMock.mockImplementation((table: string) => {
if (table === tableNameStocks) {
return {
select: () => createMockQuery([mockEntry]),
delete: () => ({ eq: () => Promise.resolve({ error: null }) }),
insert: () => Promise.resolve({ error: null })
};
}
return {
select: () => createMockQuery([])
};
});
});
beforeEach(() => {
vi.clearAllMocks();
authState.user = { id: 'user-1', email: 'test@demo.test' };
authState.profile = { role: 'user' };
vi.stubGlobal('confirm', confirmMock);
vi.stubGlobal('crypto', { randomUUID: () => 'uuid-123' });
fetchManualEntriesMock.mockResolvedValue([mockEntry]);
deleteManualEntryMock.mockResolvedValue(undefined);
createManualEntryMock.mockResolvedValue({});
});
it('handles entry drawer interactions', async () => {
const user = userEvent.setup();
@ -109,14 +88,14 @@ describe('EntriesTab master suite', () => {
it('derives diverse entry states (LOCKED, BLOCKED, SUBMITTED, CONFIRMED, ORPHAN)', async () => {
const user = userEvent.setup();
const entries = [
{ stock_instance_id: 's1', symbol: 'S1', active: true, status: 'active', is_real_trade: false },
{ stock_instance_id: 's2', symbol: 'S2', active: false, status: 'cooldown', is_real_trade: false }, // Use 'cooldown' instead of 'blocked' because 'blocked' contains 'lock'
const entries = [
{ stock_instance_id: 's1', symbol: 'S1', active: true, status: 'active', is_real_trade: false },
{ stock_instance_id: 's2', symbol: 'S2', active: false, status: 'cooldown', is_real_trade: false }, // Use 'cooldown' instead of 'blocked' because 'blocked' contains 'lock'
{ stock_instance_id: 's3', symbol: 'S3', active: false, status: 'submitted', is_real_trade: false },
{ stock_instance_id: 's4', symbol: 'S4', active: false, status: 'confirmed', is_real_trade: false },
{ stock_instance_id: 's5', symbol: 'S5', active: false, status: 'orphan', is_real_trade: false },
];
fromMock.mockImplementation(() => ({ select: () => createMockQuery(entries) }));
{ stock_instance_id: 's5', symbol: 'S5', active: false, status: 'orphan', is_real_trade: false },
];
fetchManualEntriesMock.mockResolvedValue(entries);
render(<EntriesTab botState={mockBotState} />);
@ -138,13 +117,13 @@ describe('EntriesTab master suite', () => {
await waitFor(() => expect(screen.getByText('BTC/USDT')).toBeInTheDocument());
const card = screen.getByText('BTC/USDT').closest('.group');
const buttons = within(card as HTMLElement).getAllByRole('button');
await fireEvent.click(buttons[1]);
expect(fromMock).toHaveBeenCalledWith(tableNameStocks);
confirmMock.mockReturnValue(true);
await fireEvent.click(buttons[2]);
expect(fromMock).toHaveBeenCalledWith(tableNameStocks);
});
});
const buttons = within(card as HTMLElement).getAllByRole('button');
await fireEvent.click(buttons[1]);
expect(createManualEntryMock).toHaveBeenCalled();
confirmMock.mockReturnValue(true);
await fireEvent.click(buttons[2]);
expect(deleteManualEntryMock).toHaveBeenCalledWith('entry-1');
});
});

View File

@ -1,11 +1,10 @@
import { useEffect, useState } from 'react';
import { supabase } from '../lib/supabaseClient';
import { useAuth } from '../components/AuthContext';
import { tableNameStocks } from '../lib/const';
import { EntryForm } from '../components/EntryForm';
import { Pencil, Trash2, Copy as CopyIcon, Search, Plus, Filter, LayoutGrid, Clock, ShieldCheck, Activity } from 'lucide-react';
import type { BotState } from '../hooks/useWebSocket';
import { DEFAULT_BOT_STATE } from '../hooks/useWebSocket';
import { useEffect, useState } from 'react';
import { useAuth } from '../components/AuthContext';
import { EntryForm } from '../components/EntryForm';
import { Pencil, Trash2, Copy as CopyIcon, Search, Plus, Filter, LayoutGrid, Clock, ShieldCheck, Activity } from 'lucide-react';
import type { BotState } from '../hooks/useWebSocket';
import { DEFAULT_BOT_STATE } from '../hooks/useWebSocket';
import { createManualEntry, deleteManualEntry, fetchManualEntries, type ManualEntryPayload } from '../lib/manualEntriesApi';
interface Entry {
stock_instance_id: string;
@ -105,20 +104,14 @@ export const EntriesTab = ({ botState = DEFAULT_BOT_STATE }: EntriesTabProps) =>
// Filter Logic
const filteredEntries = filterEntriesByTab(entries, activeTab);
const fetchEntries = async () => {
if (!user) return;
try {
const { data, error } = await supabase
.from(tableNameStocks)
.select("*")
.eq("user_id", user.id)
.order("created_at", { ascending: false });
if (error) throw error;
setEntries(data || []);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
console.error("Error fetching entries:", message);
const fetchEntries = async () => {
if (!user) return;
try {
const data = await fetchManualEntries();
setEntries(data as Entry[]);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
console.error("Error fetching entries:", message);
}
};
@ -126,18 +119,16 @@ export const EntriesTab = ({ botState = DEFAULT_BOT_STATE }: EntriesTabProps) =>
fetchEntries();
}, [user]);
const handleDelete = async (id: string) => {
if (!confirm('⚠️ CRITICAL: Permanently delete this entry from neural watchlist?')) return;
await supabase.from(tableNameStocks).delete().eq('stock_instance_id', id);
fetchEntries();
};
const handleClone = async (entry: Entry) => {
await supabase.from(tableNameStocks).insert([{
...buildClonedEntryPayload(entry, crypto.randomUUID())
}]);
fetchEntries();
};
const handleDelete = async (id: string) => {
if (!confirm('⚠️ CRITICAL: Permanently delete this entry from neural watchlist?')) return;
await deleteManualEntry(id);
fetchEntries();
};
const handleClone = async (entry: Entry) => {
await createManualEntry(buildClonedEntryPayload(entry, crypto.randomUUID()) as unknown as ManualEntryPayload);
fetchEntries();
};
const entryCards = filteredEntries.map((entry) => {
const entryState = deriveEntryState(entry, botState);