'use client'; import { type ReactNode, useCallback, useMemo, useState } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList, type ListChildComponentProps } from 'react-window'; import { Badge, Box, Button, CloseButton, Drawer, Flex, Portal, SimpleGrid, Text, } from '@chakra-ui/react'; export interface OrdersDrawerSummaryItem { title: string; value: ReactNode; color?: string; } export interface OrdersDrawerMetric { title: string; value: ReactNode; detail?: ReactNode; detailLines?: readonly string[]; color?: string; } export interface OrdersDrawerOrder { id: string; title: string; reference?: ReactNode; subtitle?: ReactNode; period?: { start: number | null | undefined; end?: number | null; durationHours?: number | null; }; direction?: 'LONG' | 'SHORT' | null; status?: 'active' | 'closed'; statusLabel?: string; statusColor?: string; pnl?: number | null; accentColor?: string; metrics: OrdersDrawerMetric[]; } interface OrdersDrawerPanelProps { title: string; open: boolean; orders: OrdersDrawerOrder[]; emptyText?: string; rowHeight?: number; showStatusFilter?: boolean; statusFilterOptions?: readonly { label: string; value: OrderStatusFilter; }[]; onOpenChange: (open: boolean) => void; } const DEFAULT_ROW_HEIGHT = 278; const ORDER_LIST_OVERSCAN = 4; type OrderStatusFilter = 'all' | 'closed' | 'active'; type OrderDirectionFilter = 'all' | 'LONG' | 'SHORT'; const orderStatusFilterOptions = [ { label: 'All', value: 'all' }, { label: 'Closed', value: 'closed' }, { label: 'Active', value: 'active' }, ] as const; const orderDirectionFilterOptions = [ { label: 'All', value: 'all' }, { label: 'Long', value: 'LONG' }, { label: 'Short', value: 'SHORT' }, ] as const; export const formatDateTime = (value: number | null | undefined) => { if (typeof value !== 'number' || !Number.isFinite(value)) { return 'n/a'; } return new Date(value).toLocaleString('ru-RU'); }; export const formatSignedNumber = (value: number | null | undefined) => { if (typeof value !== 'number' || !Number.isFinite(value)) { return 'n/a'; } return `${value > 0 ? '+' : ''}${value.toLocaleString('ru-RU', { maximumFractionDigits: 2, minimumFractionDigits: 2, })}`; }; export const formatCompactNumber = ( value: number | null | undefined, options: Intl.NumberFormatOptions = {}, ) => { if (typeof value !== 'number' || !Number.isFinite(value)) { return 'n/a'; } return value.toLocaleString('ru-RU', { maximumFractionDigits: 8, ...options, }); }; export const formatUsdt = ( value: number | null | undefined, { signed = false, maximumFractionDigits = 2, minimumFractionDigits = 2, }: Intl.NumberFormatOptions & { signed?: boolean } = {}, ) => { if (typeof value !== 'number' || !Number.isFinite(value)) { return 'n/a'; } const prefix = signed && value > 0 ? '+' : ''; return `${prefix}${value.toLocaleString('ru-RU', { maximumFractionDigits, minimumFractionDigits, })} USDT`; }; export const formatPriceUsdt = (value: number | null | undefined) => formatUsdt(value, { maximumFractionDigits: 8, minimumFractionDigits: 0, }); export const formatInteger = (value: number | null | undefined) => { if (typeof value !== 'number' || !Number.isFinite(value)) { return 'n/a'; } return value.toLocaleString('ru-RU', { maximumFractionDigits: 0, }); }; export const formatDuration = (hours: number | null | undefined) => { if (typeof hours !== 'number' || !Number.isFinite(hours)) { return 'n/a'; } if (hours < 24) { return `${formatCompactNumber(hours, { maximumFractionDigits: 1, minimumFractionDigits: 1, })}h`; } const days = Math.floor(hours / 24); const remainderHours = Math.round(hours % 24); return `${days}d ${remainderHours}h`; }; export const formatOrderPeriod = ({ start, end, durationHours, }: NonNullable) => { const startLabel = formatDateTime(start); if (typeof end !== 'number' || !Number.isFinite(end)) { return startLabel; } const durationLabel = typeof durationHours === 'number' && Number.isFinite(durationHours) ? ` (${formatDuration(durationHours)})` : ''; return `${startLabel} - ${formatDateTime(end)}${durationLabel}`; }; export const formatFee = (value: number | null | undefined) => { return formatUsdt(value, { signed: true, maximumFractionDigits: 8, minimumFractionDigits: 0, }); }; export const formatPercent = ( value: number | null | undefined, { signed = false }: { signed?: boolean } = {}, ) => { if (typeof value !== 'number' || !Number.isFinite(value)) { return 'n/a'; } const prefix = signed && value > 0 ? '+' : ''; return `${prefix}${value.toLocaleString('ru-RU', { maximumFractionDigits: 2, minimumFractionDigits: 2, })}%`; }; export const getPnlColor = (value: number | null | undefined) => { if (typeof value !== 'number' || !Number.isFinite(value) || value === 0) { return 'gray.300'; } return value > 0 ? 'teal.300' : 'red.300'; }; const getDirectionPalette = (direction: OrdersDrawerOrder['direction']) => direction === 'LONG' ? 'teal' : 'red'; const filterOrders = ({ orders, statusFilter, directionFilter, }: { orders: OrdersDrawerOrder[]; statusFilter: OrderStatusFilter; directionFilter: OrderDirectionFilter; }) => orders.filter((order) => { if (statusFilter !== 'all' && order.status !== statusFilter) { return false; } if (directionFilter !== 'all' && order.direction !== directionFilter) { return false; } return true; }); interface OrdersFilterGroupProps { label: string; options: readonly { label: string; value: TValue }[]; value: TValue; onChange: (value: TValue) => void; } const OrdersFilterGroup = ({ label, options, value, onChange, }: OrdersFilterGroupProps) => ( {label} {options.map((option) => { const selected = option.value === value; return ( ); })} ); const OrdersFilters = ({ statusFilter, directionFilter, showStatusFilter, statusFilterOptions, onStatusFilterChange, onDirectionFilterChange, }: { statusFilter: OrderStatusFilter; directionFilter: OrderDirectionFilter; showStatusFilter: boolean; statusFilterOptions: readonly { label: string; value: OrderStatusFilter; }[]; onStatusFilterChange: (value: OrderStatusFilter) => void; onDirectionFilterChange: (value: OrderDirectionFilter) => void; }) => ( {showStatusFilter ? ( ) : null} ); const OrderMetric = ({ title, value, detail, detailLines, color = 'gray.300', }: OrdersDrawerMetric) => ( {title} {value} {detail || detailLines?.length ? ( {detailLines?.map((line) => {line}) ?? detail} ) : null} ); const OrderCard = ({ order }: { order: OrdersDrawerOrder }) => { const accentColor = order.accentColor ?? getPnlColor(order.pnl); const subtitle = order.period ? formatOrderPeriod(order.period) : order.subtitle; return ( {order.title} {order.direction ? ( {order.direction} ) : null} {order.statusLabel ? ( {order.statusLabel} ) : null} {order.reference ? ( ref {order.reference} ) : null} {subtitle ? ( {subtitle} ) : null} P&L {formatUsdt(order.pnl, { signed: true })} {order.metrics.map((metric) => ( ))} ); }; const OrdersList = ({ orders, rowHeight, }: { orders: OrdersDrawerOrder[]; rowHeight: number; }) => { const itemKey = useCallback( (index: number) => orders[index]?.id ?? String(index), [orders], ); const Row = useCallback( ({ index, style }: ListChildComponentProps) => { const order = orders[index]; if (!order) { return null; } return ( ); }, [orders], ); return ( {({ height, width }) => ( {Row} )} ); }; export const OrdersDrawerPanel = ({ title, open, orders, emptyText = 'No orders for the selected period.', rowHeight = DEFAULT_ROW_HEIGHT, showStatusFilter = true, statusFilterOptions = orderStatusFilterOptions, onOpenChange, }: OrdersDrawerPanelProps) => { const [statusFilter, setStatusFilter] = useState('all'); const [directionFilter, setDirectionFilter] = useState('all'); const filteredOrders = useMemo( () => filterOrders({ orders, statusFilter: showStatusFilter ? statusFilter : 'all', directionFilter, }), [directionFilter, orders, showStatusFilter, statusFilter], ); return ( onOpenChange(event.open)} > {title} {orders.length === 0 ? ( {emptyText} ) : filteredOrders.length === 0 ? ( No orders match the selected filters. ) : ( )} ); };