Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
234 changes: 228 additions & 6 deletions src/3-widgets/transaction/TransactionList/GrouppedList.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import React, { FC, useCallback, useEffect, useRef, useState } from 'react'
import React, { FC, useCallback, useEffect, useRef, useState, useMemo } from 'react'
import { ListChildComponentProps, VariableSizeList } from 'react-window'
import {
StaticDatePicker,
StaticDatePickerProps,
} from '@mui/x-date-pickers/StaticDatePicker'
import AutoSizer, { VerticalSize } from 'react-virtualized-auto-sizer'
import { ListSubheader } from '@mui/material'
import { ListSubheader, Box, Typography } from '@mui/material'
import { formatDate, parseDate } from '6-shared/helpers/date'
import { TDateDraft, TISODate } from '6-shared/types'
import { TDateDraft, TISODate, TTransaction } from '6-shared/types'
import { toISODate } from '6-shared/helpers/date'
import { SmartDialog } from '6-shared/ui/SmartDialog'
import { registerPopover } from '6-shared/historyPopovers'
import { trModel, TrType } from '5-entities/transaction'
import { displayCurrency } from '5-entities/currency/displayCurrency'
import { instrumentModel } from '5-entities/currency/instrument'
import { accountModel } from '5-entities/account'
import { useTranslation } from 'react-i18next'

type GroupNode = {
date: TISODate
transactions: JSX.Element[]
rawTransactions?: TTransaction[]
}

const HEADER_HEIGHT = 48
Expand Down Expand Up @@ -113,17 +119,233 @@ type DayData = {
groups: GroupNode[]
onDateClick: (date: TISODate) => void
}

// Компонент для отображения общей суммы за день
const DayTotal: FC<{ transactions: TTransaction[] }> = ({ transactions }) => {
const { t } = useTranslation('transaction')
const toDisplay = displayCurrency.useToDisplay('current')
const instCodeMap = instrumentModel.useInstCodeMap()
const debtAccId = accountModel.useDebtAccountId()
const [showDetails, setShowDetails] = useState(false)
const [isHovered, setIsHovered] = useState(false)

// Расчитываем общую сумму за день
const total = useMemo(() => {
// Общие суммы по каждому типу транзакций
let incomeTotal = 0
let outcomeTotal = 0
let hasIncome = false
let hasOutcome = false

transactions.forEach(tr => {
const type = trModel.getType(tr, debtAccId)
const incomeCurrency = instCodeMap[tr.incomeInstrument]
const outcomeCurrency = instCodeMap[tr.outcomeInstrument]

try {
switch (type) {
case TrType.Income:
const incomeAmount = toDisplay({ [incomeCurrency]: tr.income })
if (typeof incomeAmount === 'number' && !isNaN(incomeAmount)) {
incomeTotal += incomeAmount
hasIncome = true
}
break
case TrType.Outcome:
const outcomeAmount = toDisplay({ [outcomeCurrency]: tr.outcome })
if (typeof outcomeAmount === 'number' && !isNaN(outcomeAmount)) {
outcomeTotal += outcomeAmount
hasOutcome = true
}
break
// Другие типы транзакций можно обработать по необходимости
}
} catch (error) {
console.error('Error processing transaction', tr.id, error)
}
})

// Определяем, какую сумму показывать
let totalValue;
let isPositive;

if (hasIncome && !hasOutcome) {
// Только доходы - показываем сумму доходов
totalValue = incomeTotal;
isPositive = true;
} else if (!hasIncome && hasOutcome) {
// Только расходы - показываем сумму расходов с минусом
totalValue = -outcomeTotal;
isPositive = false;
} else {
// Смешанный день - показываем разницу
totalValue = incomeTotal - outcomeTotal;
isPositive = totalValue > 0;
}

return {
income: incomeTotal,
outcome: outcomeTotal,
total: totalValue,
isPositive,
hasIncome,
hasOutcome
};
}, [transactions, debtAccId, instCodeMap, toDisplay])

// Если нет транзакций, ничего не выводим
if (!transactions?.length) return null

const displayValue = total.total

// Форматирование суммы для отображения
let formattedValue = ''
try {
if (typeof displayValue === 'number' && !isNaN(displayValue)) {
if (Math.abs(displayValue) < 0.01) {
// Если значение очень маленькое, не показываем
return null
}
// Показываем абсолютное значение, знак добавим отдельно
formattedValue = Math.abs(displayValue).toLocaleString('ru-RU')
} else {
return null;
}
} catch (error) {
return null;
}

// Форматирование значений дохода и расхода для детального отображения
const formatIncome = () => {
if (!total.income) return '0'
return total.income.toLocaleString('ru-RU')
}

const formatOutcome = () => {
if (!total.outcome) return '0'
return total.outcome.toLocaleString('ru-RU')
}

// Обработчик клика по сумме
const handleClick = (e: React.MouseEvent) => {
// Предотвращаем стандартное поведение и всплытие события
e.preventDefault();
e.stopPropagation();

// Показываем детали на 3 секунды
setShowDetails(true);
setTimeout(() => {
setShowDetails(false);
}, 3000);

// Дополнительная проверка, чтобы клик обрабатывался только здесь
return false;
}

// Обработчики тач-событий для мобильных устройств
const handleTouchStart = (e: React.TouchEvent) => {
e.stopPropagation();
setShowDetails(true);
}

const handleTouchEnd = (e: React.TouchEvent) => {
e.stopPropagation();
// Устанавливаем таймер, чтобы пользователь успел увидеть информацию
setTimeout(() => {
setShowDetails(false);
}, 3000);
}

return (
<div
style={{ cursor: 'pointer' }}
onClick={handleClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
{showDetails || isHovered ? (
<Box sx={{ display: 'flex', gap: 1 }}>
{total.hasIncome && (
<Typography
variant="body2"
color="success.main"
title={t('type_income')}
sx={{
borderRadius: '4px',
padding: '2px 6px',
backgroundColor: 'rgba(76, 175, 80, 0.12)'
}}
>
+{formatIncome()}
</Typography>
)}
{total.hasOutcome && (
<Typography
variant="body2"
color="error.main"
title={t('type_outcome')}
sx={{
borderRadius: '4px',
padding: '2px 6px',
backgroundColor: 'rgba(244, 67, 54, 0.12)'
}}
>
-{formatOutcome()}
</Typography>
)}
</Box>
) : (
<Typography
variant="body2"
color="text.secondary"
title={t('dailyTotal')}
sx={{
borderRadius: '4px',
padding: '2px 6px'
}}
>
{total.isPositive ? '+' : '-'}{formattedValue}
</Typography>
)}
</div>
)
}

const Day: FC<ListChildComponentProps<DayData>> = props => {
const { index, style, data, isScrolling } = props
const { groups, onDateClick } = data
const renderContent = useRenderState(isScrolling)
const date = groups[index].date
const length = groups[index].transactions.length
const rawTransactions = groups[index].rawTransactions

// Обработчик клика по дате
const handleDateClick = (e: React.MouseEvent) => {
onDateClick(date);
};

return (
<div style={{ ...groupStyle, ...style }}>
<ListSubheader onClick={() => onDateClick(date)}>
{formatDate(date)}
</ListSubheader>
<div style={{ position: 'relative' }}>
<ListSubheader
onClick={handleDateClick}
sx={{ cursor: 'pointer' }}
>
{formatDate(date)}
</ListSubheader>

{/* Абсолютно позиционированный элемент для суммы */}
<div style={{
position: 'absolute',
right: '16px',
top: '14px',
zIndex: 2
}}>
{rawTransactions && <DayTotal transactions={rawTransactions} />}
</div>
</div>

{renderContent ? (
groups[index].transactions
Expand Down
5 changes: 3 additions & 2 deletions src/3-widgets/transaction/TransactionList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export const TransactionList: FC<TTransactionListProps> = props => {
}, [onSelectSimilar, checkedDate])

const groups = useMemo(() => {
let groups: ByDate<{ date: TISODate; transactions: JSX.Element[] }> = {}
let groups: ByDate<{ date: TISODate; transactions: JSX.Element[]; rawTransactions: TTransaction[] }> = {}
trList.forEach(tr => {
let Component = (
<Transaction
Expand All @@ -146,8 +146,9 @@ export const TransactionList: FC<TTransactionListProps> = props => {
}
/>
)
groups[tr.date] ??= { date: tr.date, transactions: [] }
groups[tr.date] ??= { date: tr.date, transactions: [], rawTransactions: [] }
groups[tr.date].transactions.push(Component)
groups[tr.date].rawTransactions.push(tr)
})
return Object.values(groups)
}, [
Expand Down
1 change: 1 addition & 0 deletions src/6-shared/localization/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,7 @@ export const en: typeof ru = {
btnClose: 'Close',
rate: 'Rate: {{rate}}',
fullEmptyState: 'Select a transaction to see details',
dailyTotal: 'Daily total'
},

transactionsBulkEdit: {
Expand Down
3 changes: 2 additions & 1 deletion src/6-shared/localization/translations/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,8 @@
"btnSave": "Сохранить",
"btnClose": "Закрыть",
"rate": "Курс: {{rate}}",
"fullEmptyState": "Выберите операцию,\nчтобы увидеть детали"
"fullEmptyState": "Выберите операцию,\nчтобы увидеть детали",
"dailyTotal": "Итого за день"
},

"transactionsBulkEdit": {
Expand Down