refactor: move profile manager onto control plane
This commit is contained in:
parent
535e0a88a9
commit
5b59257a4b
@ -1668,14 +1668,18 @@ export class ApiServer {
|
||||
});
|
||||
|
||||
this.app.post('/api/profiles', this.requireAuth, async (req, res) => {
|
||||
const authUserId = (req as AuthenticatedRequest).authUserId;
|
||||
const authReq = req as AuthenticatedRequest;
|
||||
const authUserId = authReq.authUserId;
|
||||
if (!authUserId) {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const profile = await saveTradeProfileForUser(req.body || {}, authUserId, supabaseService);
|
||||
const isAdmin = await isTradingAdmin(authUserId, authReq.authRole);
|
||||
const requestedUserId = String(req.body?.user_id || '').trim();
|
||||
const targetUserId = isAdmin && requestedUserId ? requestedUserId : authUserId;
|
||||
const profile = await saveTradeProfileForUser(req.body || {}, targetUserId, supabaseService);
|
||||
res.status(201).json({ profile });
|
||||
} catch (error: any) {
|
||||
res.status(400).json({ error: `Failed to save profile: ${error.message}` });
|
||||
@ -1683,25 +1687,8 @@ export class ApiServer {
|
||||
});
|
||||
|
||||
this.app.put('/api/profiles/:id', this.requireAuth, async (req, res) => {
|
||||
const authUserId = (req as AuthenticatedRequest).authUserId;
|
||||
if (!authUserId) {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const profile = await saveTradeProfileForUser({
|
||||
...(req.body || {}),
|
||||
id: String(req.params.id || '').trim(),
|
||||
}, authUserId, supabaseService);
|
||||
res.json({ profile });
|
||||
} catch (error: any) {
|
||||
res.status(400).json({ error: `Failed to update profile: ${error.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
this.app.patch('/api/profiles/:id/active', this.requireAuth, async (req, res) => {
|
||||
const authUserId = (req as AuthenticatedRequest).authUserId;
|
||||
const authReq = req as AuthenticatedRequest;
|
||||
const authUserId = authReq.authUserId;
|
||||
if (!authUserId) {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
return;
|
||||
@ -1709,7 +1696,42 @@ export class ApiServer {
|
||||
|
||||
try {
|
||||
const profileId = String(req.params.id || '').trim();
|
||||
const existing = (await listTradeProfilesForUser(authUserId, supabaseService))
|
||||
const isAdmin = await isTradingAdmin(authUserId, authReq.authRole);
|
||||
const existingProfiles = isAdmin
|
||||
? await listAllTradeProfiles(supabaseService)
|
||||
: await listTradeProfilesForUser(authUserId, supabaseService);
|
||||
const existing = existingProfiles.find((profile) => profile.id === profileId);
|
||||
if (!existing) {
|
||||
res.status(404).json({ error: 'Profile not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const profile = await saveTradeProfileForUser({
|
||||
...existing,
|
||||
...(req.body || {}),
|
||||
id: profileId,
|
||||
}, existing.user_id, supabaseService);
|
||||
res.json({ profile });
|
||||
} catch (error: any) {
|
||||
res.status(400).json({ error: `Failed to update profile: ${error.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
this.app.patch('/api/profiles/:id/active', this.requireAuth, async (req, res) => {
|
||||
const authReq = req as AuthenticatedRequest;
|
||||
const authUserId = authReq.authUserId;
|
||||
if (!authUserId) {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const profileId = String(req.params.id || '').trim();
|
||||
const isAdmin = await isTradingAdmin(authUserId, authReq.authRole);
|
||||
const existingProfiles = isAdmin
|
||||
? await listAllTradeProfiles(supabaseService)
|
||||
: await listTradeProfilesForUser(authUserId, supabaseService);
|
||||
const existing = existingProfiles
|
||||
.find((profile) => profile.id === profileId);
|
||||
if (!existing) {
|
||||
res.status(404).json({ error: 'Profile not found' });
|
||||
@ -1719,7 +1741,7 @@ export class ApiServer {
|
||||
const profile = await saveTradeProfileForUser({
|
||||
...existing,
|
||||
is_active: Boolean(req.body?.is_active),
|
||||
}, authUserId, supabaseService);
|
||||
}, existing.user_id, supabaseService);
|
||||
res.json({ profile });
|
||||
} catch (error: any) {
|
||||
res.status(400).json({ error: `Failed to update profile state: ${error.message}` });
|
||||
@ -1727,14 +1749,26 @@ export class ApiServer {
|
||||
});
|
||||
|
||||
this.app.delete('/api/profiles/:id', this.requireAuth, async (req, res) => {
|
||||
const authUserId = (req as AuthenticatedRequest).authUserId;
|
||||
const authReq = req as AuthenticatedRequest;
|
||||
const authUserId = authReq.authUserId;
|
||||
if (!authUserId) {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteTradeProfileForUser(String(req.params.id || '').trim(), authUserId, supabaseService);
|
||||
const profileId = String(req.params.id || '').trim();
|
||||
const isAdmin = await isTradingAdmin(authUserId, authReq.authRole);
|
||||
const existingProfiles = isAdmin
|
||||
? await listAllTradeProfiles(supabaseService)
|
||||
: await listTradeProfilesForUser(authUserId, supabaseService);
|
||||
const existing = existingProfiles.find((profile) => profile.id === profileId);
|
||||
if (!existing) {
|
||||
res.status(404).json({ error: 'Profile not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteTradeProfileForUser(profileId, existing.user_id, supabaseService);
|
||||
res.json({ success: true });
|
||||
} catch (error: any) {
|
||||
res.status(400).json({ error: `Failed to delete profile: ${error.message}` });
|
||||
|
||||
@ -3,35 +3,31 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { TradeProfileManager } from './TradeProfileManager';
|
||||
import { tableNameProfiles, tableNameTransactions, tableNameUsers } from '../lib/const';
|
||||
import { tableNameTransactions } from '../lib/const';
|
||||
|
||||
const {
|
||||
authState,
|
||||
fromMock,
|
||||
profilesSelectMock,
|
||||
profilesOrderMock,
|
||||
profilesInsertMock,
|
||||
profilesUpdateMock,
|
||||
profilesUpdateEqMock,
|
||||
profilesDeleteMock,
|
||||
profilesDeleteEqMock,
|
||||
usersSelectMock,
|
||||
historySelectMock
|
||||
historySelectMock,
|
||||
fetchTradeProfilesMock,
|
||||
fetchCurrentUserProfileMock,
|
||||
createTradeProfileMock,
|
||||
updateTradeProfileMock,
|
||||
deleteTradeProfileMock,
|
||||
setTradeProfileActiveMock
|
||||
} = vi.hoisted(() => ({
|
||||
authState: {
|
||||
user: { id: 'user-1', email: 'owner@demo.test' } as any,
|
||||
profile: { role: 'admin' } as any
|
||||
},
|
||||
fromMock: vi.fn(),
|
||||
profilesSelectMock: vi.fn(),
|
||||
profilesOrderMock: vi.fn(),
|
||||
profilesInsertMock: vi.fn(),
|
||||
profilesUpdateMock: vi.fn(),
|
||||
profilesUpdateEqMock: vi.fn(),
|
||||
profilesDeleteMock: vi.fn(),
|
||||
profilesDeleteEqMock: vi.fn(),
|
||||
usersSelectMock: vi.fn(),
|
||||
historySelectMock: vi.fn()
|
||||
historySelectMock: vi.fn(),
|
||||
fetchTradeProfilesMock: vi.fn(),
|
||||
fetchCurrentUserProfileMock: vi.fn(),
|
||||
createTradeProfileMock: vi.fn(),
|
||||
updateTradeProfileMock: vi.fn(),
|
||||
deleteTradeProfileMock: vi.fn(),
|
||||
setTradeProfileActiveMock: vi.fn()
|
||||
}));
|
||||
|
||||
vi.mock('./AuthContext', () => ({
|
||||
@ -44,57 +40,60 @@ vi.mock('../lib/supabaseClient', () => ({
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('../lib/profileApi', () => ({
|
||||
fetchTradeProfiles: fetchTradeProfilesMock,
|
||||
fetchCurrentUserProfile: fetchCurrentUserProfileMock,
|
||||
createTradeProfile: createTradeProfileMock,
|
||||
updateTradeProfile: updateTradeProfileMock,
|
||||
deleteTradeProfile: deleteTradeProfileMock,
|
||||
setTradeProfileActive: setTradeProfileActiveMock
|
||||
}));
|
||||
|
||||
describe('TradeProfileManager DOM flow', () => {
|
||||
beforeEach(() => {
|
||||
authState.user = { id: 'user-1', email: 'owner@demo.test' };
|
||||
authState.profile = { role: 'admin' };
|
||||
|
||||
fromMock.mockReset();
|
||||
profilesSelectMock.mockReset();
|
||||
profilesOrderMock.mockReset();
|
||||
profilesInsertMock.mockReset();
|
||||
profilesUpdateMock.mockReset();
|
||||
profilesUpdateEqMock.mockReset();
|
||||
profilesDeleteMock.mockReset();
|
||||
profilesDeleteEqMock.mockReset();
|
||||
usersSelectMock.mockReset();
|
||||
historySelectMock.mockReset();
|
||||
fetchTradeProfilesMock.mockReset();
|
||||
fetchCurrentUserProfileMock.mockReset();
|
||||
createTradeProfileMock.mockReset();
|
||||
updateTradeProfileMock.mockReset();
|
||||
deleteTradeProfileMock.mockReset();
|
||||
setTradeProfileActiveMock.mockReset();
|
||||
|
||||
profilesOrderMock.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
id: 'profile-1',
|
||||
user_id: 'user-1',
|
||||
name: 'High Risk Scalper',
|
||||
allocated_capital: 3000,
|
||||
risk_per_trade_percent: 1.5,
|
||||
symbols: 'BTC/USDT, ETH/USDT',
|
||||
is_active: true,
|
||||
strategy_config: {
|
||||
rules: [
|
||||
{ ruleId: 'TrendBiasRule', enabled: true, params: { fastPeriod: 50, slowPeriod: 200 } },
|
||||
{ ruleId: 'MomentumRule', enabled: true, params: { rsiPeriod: 14, overbought: 70, oversold: 30 } }
|
||||
],
|
||||
riskLimits: {
|
||||
maxDailyLossUsd: 100,
|
||||
maxOpenTrades: 3,
|
||||
maxConsecutiveLosses: 2
|
||||
},
|
||||
execution: {
|
||||
orderType: 'market',
|
||||
cooldownMinutes: 30,
|
||||
entryMode: 'both'
|
||||
}
|
||||
fetchTradeProfilesMock.mockResolvedValue([
|
||||
{
|
||||
id: 'profile-1',
|
||||
user_id: 'user-1',
|
||||
name: 'High Risk Scalper',
|
||||
allocated_capital: 3000,
|
||||
risk_per_trade_percent: 1.5,
|
||||
symbols: 'BTC/USDT, ETH/USDT',
|
||||
is_active: true,
|
||||
strategy_config: {
|
||||
rules: [
|
||||
{ ruleId: 'TrendBiasRule', enabled: true, params: { fastPeriod: 50, slowPeriod: 200 } },
|
||||
{ ruleId: 'MomentumRule', enabled: true, params: { rsiPeriod: 14, overbought: 70, oversold: 30 } }
|
||||
],
|
||||
riskLimits: {
|
||||
maxDailyLossUsd: 100,
|
||||
maxOpenTrades: 3,
|
||||
maxConsecutiveLosses: 2
|
||||
},
|
||||
created_at: '2026-02-15T10:00:00.000Z'
|
||||
}
|
||||
],
|
||||
error: null
|
||||
});
|
||||
|
||||
usersSelectMock.mockResolvedValue({
|
||||
data: [{ user_id: 'user-1', email: 'owner@demo.test' }],
|
||||
error: null
|
||||
execution: {
|
||||
orderType: 'market',
|
||||
cooldownMinutes: 30,
|
||||
entryMode: 'both'
|
||||
}
|
||||
},
|
||||
created_at: '2026-02-15T10:00:00.000Z'
|
||||
}
|
||||
]);
|
||||
fetchCurrentUserProfileMock.mockResolvedValue({
|
||||
user_id: 'user-1',
|
||||
email: 'owner@demo.test'
|
||||
});
|
||||
|
||||
historySelectMock.mockResolvedValue({
|
||||
@ -119,27 +118,12 @@ describe('TradeProfileManager DOM flow', () => {
|
||||
error: null
|
||||
});
|
||||
|
||||
profilesInsertMock.mockResolvedValue({ error: null });
|
||||
profilesUpdateEqMock.mockResolvedValue({ error: null });
|
||||
profilesUpdateMock.mockReturnValue({ eq: profilesUpdateEqMock });
|
||||
profilesDeleteEqMock.mockResolvedValue({ error: null });
|
||||
profilesDeleteMock.mockReturnValue({ eq: profilesDeleteEqMock });
|
||||
profilesSelectMock.mockReturnValue({ order: profilesOrderMock });
|
||||
createTradeProfileMock.mockResolvedValue({ id: 'profile-2' });
|
||||
updateTradeProfileMock.mockResolvedValue({ id: 'profile-1' });
|
||||
deleteTradeProfileMock.mockResolvedValue(undefined);
|
||||
setTradeProfileActiveMock.mockResolvedValue({ id: 'profile-1', is_active: false });
|
||||
|
||||
fromMock.mockImplementation((table: string) => {
|
||||
if (table === tableNameProfiles) {
|
||||
return {
|
||||
select: profilesSelectMock,
|
||||
insert: profilesInsertMock,
|
||||
update: profilesUpdateMock,
|
||||
delete: profilesDeleteMock
|
||||
};
|
||||
}
|
||||
if (table === tableNameUsers) {
|
||||
return {
|
||||
select: usersSelectMock
|
||||
};
|
||||
}
|
||||
if (table === tableNameTransactions) {
|
||||
return {
|
||||
select: historySelectMock
|
||||
@ -172,7 +156,7 @@ describe('TradeProfileManager DOM flow', () => {
|
||||
|
||||
await user.click(screen.getByTitle('Refresh'));
|
||||
await waitFor(() => {
|
||||
expect(profilesOrderMock.mock.calls.length).toBeGreaterThan(1);
|
||||
expect(fetchTradeProfilesMock.mock.calls.length).toBeGreaterThan(1);
|
||||
});
|
||||
}, 20000);
|
||||
|
||||
@ -205,15 +189,13 @@ describe('TradeProfileManager DOM flow', () => {
|
||||
await user.click(screen.getAllByRole('button', { name: 'Create Profile' }).at(-1)!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(profilesInsertMock).toHaveBeenCalledTimes(1);
|
||||
expect(profilesInsertMock).toHaveBeenCalledWith([
|
||||
expect.objectContaining({
|
||||
name: 'Night Session Guard',
|
||||
user_id: 'user-1',
|
||||
symbols: 'ETH/USDT,SOL/USDT'
|
||||
})
|
||||
]);
|
||||
const insertedPayload = profilesInsertMock.mock.calls[0][0][0];
|
||||
expect(createTradeProfileMock).toHaveBeenCalledTimes(1);
|
||||
expect(createTradeProfileMock).toHaveBeenCalledWith(expect.objectContaining({
|
||||
name: 'Night Session Guard',
|
||||
user_id: 'user-1',
|
||||
symbols: 'ETH/USDT,SOL/USDT'
|
||||
}));
|
||||
const insertedPayload = createTradeProfileMock.mock.calls[0][0];
|
||||
const sessionRule = insertedPayload?.strategy_config?.rules?.find((rule: any) => rule.ruleId === 'SessionRule');
|
||||
expect(sessionRule?.params?.sessions).toBe('24/7');
|
||||
expect(screen.getByText('Profile created successfully')).toBeInTheDocument();
|
||||
@ -230,7 +212,7 @@ describe('TradeProfileManager DOM flow', () => {
|
||||
|
||||
await user.click(screen.getByTitle('Pause profile'));
|
||||
await waitFor(() => {
|
||||
expect(profilesUpdateEqMock).toHaveBeenCalledWith('id', 'profile-1');
|
||||
expect(setTradeProfileActiveMock).toHaveBeenCalledWith('profile-1', false);
|
||||
expect(screen.getByText('Profile suspended')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@ -241,7 +223,9 @@ describe('TradeProfileManager DOM flow', () => {
|
||||
await user.click(screen.getByRole('button', { name: 'Save Changes' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(profilesUpdateEqMock).toHaveBeenCalledWith('id', 'profile-1');
|
||||
expect(updateTradeProfileMock).toHaveBeenCalledWith('profile-1', expect.objectContaining({
|
||||
name: 'High Risk Scalper v2'
|
||||
}));
|
||||
expect(screen.getByText('Profile updated')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@ -250,14 +234,14 @@ describe('TradeProfileManager DOM flow', () => {
|
||||
await user.click(screen.getAllByRole('button', { name: 'Delete' }).at(-1)!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(profilesDeleteEqMock).toHaveBeenCalledWith('id', 'profile-1');
|
||||
expect(deleteTradeProfileMock).toHaveBeenCalledWith('profile-1');
|
||||
expect(screen.getByText('Profile deleted')).toBeInTheDocument();
|
||||
});
|
||||
}, 20000);
|
||||
|
||||
it('surfaces update and delete failures in toast feedback', async () => {
|
||||
profilesUpdateEqMock.mockResolvedValueOnce({ error: { message: 'permission denied' } });
|
||||
profilesDeleteEqMock.mockResolvedValueOnce({ error: { message: 'foreign key violation' } });
|
||||
setTradeProfileActiveMock.mockRejectedValueOnce(new Error('permission denied'));
|
||||
deleteTradeProfileMock.mockRejectedValueOnce(new Error('foreign key violation'));
|
||||
const user = userEvent.setup();
|
||||
render(<TradeProfileManager />);
|
||||
|
||||
|
||||
@ -1,8 +1,17 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { supabase } from '../lib/supabaseClient';
|
||||
import { tableNameTransactions, tableNameProfiles, tableNameUsers } from '../lib/const';
|
||||
import { tableNameTransactions } from '../lib/const';
|
||||
import { aggregateHistoryLedger, buildHistoryLedger } from '../lib/tradeHistoryLedger';
|
||||
import {
|
||||
createTradeProfile,
|
||||
deleteTradeProfile,
|
||||
fetchCurrentUserProfile,
|
||||
fetchTradeProfiles,
|
||||
setTradeProfileActive,
|
||||
type TradeProfilePayload,
|
||||
updateTradeProfile
|
||||
} from '../lib/profileApi';
|
||||
import {
|
||||
Trash2, Edit3, X,
|
||||
Check, Zap, Play,
|
||||
@ -290,10 +299,10 @@ interface TradeProfileManagerProps {
|
||||
export const TradeProfileManager = ({ botState = DEFAULT_BOT_STATE }: TradeProfileManagerProps) => {
|
||||
const { user: authUser, profile } = useAuth();
|
||||
const [profiles, setProfiles] = useState<Profile[]>([]);
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [tradeStats, setTradeStats] = useState<Record<string, ProfileTradeStats>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [currentUserProfile, setCurrentUserProfile] = useState<User | null>(null);
|
||||
|
||||
// Drawer state
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||
@ -315,34 +324,31 @@ export const TradeProfileManager = ({ botState = DEFAULT_BOT_STATE }: TradeProfi
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
let profilesQuery = supabase
|
||||
.from(tableNameProfiles)
|
||||
.select('id,user_id,name,allocated_capital,risk_per_trade_percent,symbols,is_active,strategy_config,created_at')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
let usersQuery = supabase.from(tableNameUsers).select('user_id, email');
|
||||
|
||||
let historyQuery = supabase
|
||||
.from(tableNameTransactions)
|
||||
.select('id,timestamp,symbol,side,size,entry_price,exit_price,pnl,pnl_percent,reason,profile_id,created_at,trade_id,source');
|
||||
|
||||
if (profile?.role !== 'admin' && authUser?.id) {
|
||||
profilesQuery = profilesQuery.eq('user_id', authUser.id);
|
||||
usersQuery = usersQuery.eq('user_id', authUser.id);
|
||||
historyQuery = historyQuery.eq('user_id', authUser.id);
|
||||
}
|
||||
|
||||
const [pRes, uRes, hRes] = await Promise.all([
|
||||
profilesQuery,
|
||||
usersQuery,
|
||||
const [profilesData, meProfile, hRes] = await Promise.all([
|
||||
fetchTradeProfiles({ scope: profile?.role === 'admin' ? 'all' : 'user' }),
|
||||
fetchCurrentUserProfile().catch(() => null),
|
||||
historyQuery
|
||||
]);
|
||||
const normalizedProfiles = (pRes.data || []).map((profile: any) => ({
|
||||
const normalizedProfiles = (profilesData || []).map((profile: any) => ({
|
||||
...profile,
|
||||
strategy_config: normalizeStrategyConfig(profile.strategy_config as StrategyConfig)
|
||||
}));
|
||||
setProfiles(normalizedProfiles);
|
||||
setUsers(uRes.data || []);
|
||||
setCurrentUserProfile(meProfile ? {
|
||||
user_id: String(meProfile.user_id || authUser?.id || ''),
|
||||
email: String(meProfile.email || authUser?.email || '')
|
||||
} : (authUser?.id || authUser?.email ? {
|
||||
user_id: String(authUser?.id || ''),
|
||||
email: String(authUser?.email || '')
|
||||
} : null));
|
||||
|
||||
const historyLedger = buildHistoryLedger({
|
||||
dbRows: hRes.data || [],
|
||||
@ -393,7 +399,7 @@ export const TradeProfileManager = ({ botState = DEFAULT_BOT_STATE }: TradeProfi
|
||||
const handleOpenAdd = () => {
|
||||
setEditingProfile({
|
||||
name: '',
|
||||
user_id: authUser?.id || users[0]?.user_id || '',
|
||||
user_id: authUser?.id || currentUserProfile?.user_id || '',
|
||||
allocated_capital: 1000,
|
||||
risk_per_trade_percent: 1,
|
||||
is_active: true,
|
||||
@ -413,37 +419,38 @@ export const TradeProfileManager = ({ botState = DEFAULT_BOT_STATE }: TradeProfi
|
||||
if (!editingProfile.name) return addToast('Profile name is required', 'error');
|
||||
setLoading(true);
|
||||
const id = isAddingMode ? null : editingProfile.id;
|
||||
const payload = {
|
||||
const payload: TradeProfilePayload = {
|
||||
name: editingProfile.name,
|
||||
user_id: editingProfile.user_id || authUser?.id || users[0]?.user_id,
|
||||
user_id: editingProfile.user_id || authUser?.id || currentUserProfile?.user_id,
|
||||
allocated_capital: Number(editingProfile.allocated_capital),
|
||||
risk_per_trade_percent: Number(editingProfile.risk_per_trade_percent),
|
||||
symbols: editingProfile.symbols,
|
||||
symbols: editingProfile.symbols || '',
|
||||
is_active: editingProfile.is_active ?? true,
|
||||
strategy_config: normalizeStrategyConfig(editingProfile.strategy_config as StrategyConfig)
|
||||
strategy_config: normalizeStrategyConfig(editingProfile.strategy_config as StrategyConfig) as unknown as Record<string, unknown>
|
||||
};
|
||||
|
||||
const { error } = id
|
||||
? await supabase.from(tableNameProfiles).update(payload).eq('id', id)
|
||||
: await supabase.from(tableNameProfiles).insert([payload]);
|
||||
|
||||
if (error) {
|
||||
addToast(error.message, 'error');
|
||||
} else {
|
||||
try {
|
||||
if (id) {
|
||||
await updateTradeProfile(id, payload);
|
||||
} else {
|
||||
await createTradeProfile(payload);
|
||||
}
|
||||
addToast(isAddingMode ? 'Profile created successfully' : 'Profile updated', 'success');
|
||||
setIsDrawerOpen(false);
|
||||
fetchData();
|
||||
await fetchData();
|
||||
} catch (error: any) {
|
||||
addToast(error?.message || 'Failed to save profile', 'error');
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleDelete = async (profileId: string) => {
|
||||
const { error } = await supabase.from(tableNameProfiles).delete().eq('id', profileId);
|
||||
if (error) {
|
||||
addToast(`Delete failed: ${error.message}`, 'error');
|
||||
} else {
|
||||
try {
|
||||
await deleteTradeProfile(profileId);
|
||||
addToast('Profile deleted', 'success');
|
||||
fetchData();
|
||||
await fetchData();
|
||||
} catch (error: any) {
|
||||
addToast(`Delete failed: ${error?.message || 'Unknown error'}`, 'error');
|
||||
}
|
||||
setDeleteConfirm(null);
|
||||
};
|
||||
@ -471,12 +478,12 @@ export const TradeProfileManager = ({ botState = DEFAULT_BOT_STATE }: TradeProfi
|
||||
|
||||
const toggleProfileActive = async (profile: Profile) => {
|
||||
const newState = !profile.is_active;
|
||||
const { error } = await supabase.from(tableNameProfiles).update({ is_active: newState }).eq('id', profile.id);
|
||||
if (error) {
|
||||
addToast(error.message, 'error');
|
||||
} else {
|
||||
try {
|
||||
await setTradeProfileActive(profile.id, newState);
|
||||
addToast(newState ? 'Profile activated' : 'Profile suspended', 'success');
|
||||
fetchData();
|
||||
await fetchData();
|
||||
} catch (error: any) {
|
||||
addToast(error?.message || 'Failed to update profile state', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
@ -516,8 +523,13 @@ export const TradeProfileManager = ({ botState = DEFAULT_BOT_STATE }: TradeProfi
|
||||
const { activeCount, totalCapital, totalPnl, totalTrades } = summarizePortfolioStats(profiles, tradeStats);
|
||||
|
||||
const getUserEmail = (userId: string) => {
|
||||
const u = users.find(u => u.user_id === userId);
|
||||
return u ? u.email : '';
|
||||
if (userId && userId === currentUserProfile?.user_id) {
|
||||
return currentUserProfile.email;
|
||||
}
|
||||
if (userId && userId === authUser?.id) {
|
||||
return authUser?.email || '';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// --- RENDER ---
|
||||
|
||||
Loading…
Reference in New Issue
Block a user