import React, { type ComponentProps, type ReactNode, useRef, useState, } from 'react'; import { Dialog, DialogTrigger } from 'react-aria-components'; import { useHotkeys } from 'react-hotkeys-hook'; import { Trans, useTranslation } from 'react-i18next'; import { Button } from '@actual-app/components/button'; import { AnimatedLoading } from '@actual-app/components/icons/AnimatedLoading'; import { SvgAdd, SvgDotsHorizontalTriple, } from '@actual-app/components/icons/v1'; import { SvgArrowsExpand3, SvgArrowsShrink3, SvgDownloadThickBottom, SvgLockClosed, SvgPencil1, } from '@actual-app/components/icons/v2'; import { InitialFocus } from '@actual-app/components/initial-focus'; import { Input } from '@actual-app/components/input'; import { Menu } from '@actual-app/components/menu'; import { Popover } from '@actual-app/components/popover'; import { Stack } from '@actual-app/components/stack'; import { styles } from '@actual-app/components/styles'; import { theme } from '@actual-app/components/theme'; import { Tooltip } from '@actual-app/components/tooltip'; import { View } from '@actual-app/components/view'; import { format as formatDate } from 'date-fns'; import { tsToRelativeTime } from 'loot-core/shared/util'; import { type AccountEntity, type RuleConditionEntity, type TransactionEntity, type TransactionFilterEntity, } from 'loot-core/types/models'; import { type TableRef } from './Account'; import { Balances } from './Balance'; import { ReconcileMenu, ReconcilingMessage } from './Reconcile'; import { AnimatedRefresh } from '@desktop-client/components/AnimatedRefresh'; import { Search } from '@desktop-client/components/common/Search'; import { FilterButton } from '@desktop-client/components/filters/FiltersMenu'; import { FiltersStack } from '@desktop-client/components/filters/FiltersStack'; import { type SavedFilter } from '@desktop-client/components/filters/SavedFilterMenuButton'; import { NotesButton } from '@desktop-client/components/NotesButton'; import { SelectedTransactionsButton } from '@desktop-client/components/transactions/SelectedTransactionsButton'; import { useDateFormat } from '@desktop-client/hooks/useDateFormat'; import { useLocale } from '@desktop-client/hooks/useLocale'; import { useLocalPref } from '@desktop-client/hooks/useLocalPref'; import { useSplitsExpanded } from '@desktop-client/hooks/useSplitsExpanded'; import { useSyncServerStatus } from '@desktop-client/hooks/useSyncServerStatus'; type AccountHeaderProps = { tableRef: TableRef; isNameEditable: boolean; workingHard: boolean; accountName: string; account?: AccountEntity; filterId?: SavedFilter; savedFilters: TransactionFilterEntity[]; accountsSyncing: string[]; failedAccounts: AccountSyncSidebarProps['failedAccounts']; accounts: AccountEntity[]; transactions: TransactionEntity[]; showBalances: boolean; showExtraBalances: boolean; showCleared: boolean; showReconciled: boolean; showEmptyMessage: boolean; balanceQuery: ComponentProps['balanceQuery']; reconcileAmount?: number | null; canCalculateBalance?: () => boolean; isFiltered: boolean; filteredAmount?: number | null; isSorted: boolean; search: string; filterConditions: RuleConditionEntity[]; filterConditionsOp: 'and' | 'or'; onSearch: (newSearch: string) => void; onAddTransaction: () => void; onShowTransactions: ComponentProps< typeof SelectedTransactionsButton >['onShow']; onDoneReconciling: ComponentProps['onDone']; onCreateReconciliationTransaction: ComponentProps< typeof ReconcilingMessage >['onCreateTransaction']; onToggleExtraBalances: ComponentProps< typeof Balances >['onToggleExtraBalances']; onSaveName: AccountNameFieldProps['onSaveName']; saveNameError: AccountNameFieldProps['saveNameError']; onSync: () => void; onImport: () => void; onMenuSelect: AccountMenuProps['onMenuSelect']; onReconcile: ComponentProps['onReconcile']; onBatchEdit: ComponentProps['onEdit']; onRunRules: ComponentProps['onRunRules']; onBatchDelete: ComponentProps['onDelete']; onBatchDuplicate: ComponentProps< typeof SelectedTransactionsButton >['onDuplicate']; onBatchLinkSchedule: ComponentProps< typeof SelectedTransactionsButton >['onLinkSchedule']; onBatchUnlinkSchedule: ComponentProps< typeof SelectedTransactionsButton >['onUnlinkSchedule']; onApplyFilter: (filter: RuleConditionEntity) => void; } & Pick< ComponentProps, | 'onCreateRule' | 'onScheduleAction' | 'onSetTransfer' | 'onMakeAsSplitTransaction' | 'onMakeAsNonSplitTransactions' | 'onMergeTransactions' > & Pick< ComponentProps, | 'onUpdateFilter' | 'onDeleteFilter' | 'onConditionsOpChange' | 'onClearFilters' | 'onReloadSavedFilter' >; export function AccountHeader({ tableRef, isNameEditable, workingHard, accountName, account, filterId, savedFilters, accountsSyncing, failedAccounts, accounts, transactions, showBalances, showExtraBalances, showCleared, showReconciled, showEmptyMessage, balanceQuery, reconcileAmount, canCalculateBalance, isFiltered, filteredAmount, isSorted, search, filterConditions, filterConditionsOp, onSearch, onAddTransaction, onShowTransactions, onDoneReconciling, onCreateReconciliationTransaction, onToggleExtraBalances, onSaveName, saveNameError, onSync, onImport, onMenuSelect, onReconcile, onBatchDelete, onBatchDuplicate, onBatchEdit, onBatchLinkSchedule, onBatchUnlinkSchedule, onCreateRule, onApplyFilter, onUpdateFilter, onClearFilters, onReloadSavedFilter, onConditionsOpChange, onDeleteFilter, onScheduleAction, onSetTransfer, onRunRules, onMakeAsSplitTransaction, onMakeAsNonSplitTransactions, onMergeTransactions, }: AccountHeaderProps) { const { t } = useTranslation(); const [reconcileOpen, setReconcileOpen] = useState(false); const searchInput = useRef(null); const reconcileRef = useRef(null); const splitsExpanded = useSplitsExpanded(); const syncServerStatus = useSyncServerStatus(); const isUsingServer = syncServerStatus !== 'no-server'; const isServerOffline = syncServerStatus === 'offline'; const [_, setExpandSplitsPref] = useLocalPref('expand-splits'); const dateFormat = useDateFormat() || 'MM/dd/yyyy'; const locale = useLocale(); let canSync = !!(account?.account_id && isUsingServer); if (!account) { // All accounts - check for any syncable account canSync = !!accounts.find(account => !!account.account_id) && isUsingServer; } // Only show the ability to make linked transfers on multi-account views. const showMakeTransfer = !account; function onToggleSplits() { if (tableRef.current) { splitsExpanded.dispatch({ type: 'switch-mode', id: tableRef.current.getScrolledItem(), }); setExpandSplitsPref(!(splitsExpanded.state.mode === 'expand')); } } useHotkeys( 'ctrl+f, cmd+f, meta+f', () => { if (searchInput.current) { searchInput.current.focus(); } }, { enableOnFormTags: true, preventDefault: true, scopes: ['app'], }, [searchInput], ); useHotkeys( 't', () => onAddTransaction(), { preventDefault: true, scopes: ['app'], }, [onAddTransaction], ); useHotkeys( 'ctrl+i, cmd+i, meta+i', () => onImport(), { scopes: ['app'], }, [onImport], ); useHotkeys( 'ctrl+b, cmd+b, meta+b', () => onSync(), { enabled: canSync && !isServerOffline, preventDefault: true, scopes: ['app'], }, [onSync], ); return ( <> {!!account?.bank && ( )} {canSync && ( )} {account && !account.closed && ( )} {!showEmptyMessage && ( )} {/* @ts-expect-error fix me */} {workingHard ? ( ) : ( transactions.find(t => t.id === id)} onShow={onShowTransactions} onDuplicate={onBatchDuplicate} onDelete={onBatchDelete} onEdit={onBatchEdit} onRunRules={onRunRules} onLinkSchedule={onBatchLinkSchedule} onUnlinkSchedule={onBatchUnlinkSchedule} onCreateRule={onCreateRule} onSetTransfer={onSetTransfer} onScheduleAction={onScheduleAction} showMakeTransfer={showMakeTransfer} onMakeAsSplitTransaction={onMakeAsSplitTransaction} onMakeAsNonSplitTransactions={onMakeAsNonSplitTransactions} onMergeTransactions={onMergeTransactions} /> )} {account && ( setReconcileOpen(false)} > setReconcileOpen(false)} onReconcile={onReconcile} /> )} {account ? ( ) : ( )} {filterConditions?.length > 0 && ( )} {reconcileAmount != null && ( )} ); } type AccountSyncSidebarProps = { account: AccountEntity; failedAccounts: Map< string, { type: string; code: string; } >; accountsSyncing: string[]; }; function AccountSyncSidebar({ account, failedAccounts, accountsSyncing, }: AccountSyncSidebarProps) { return ( ); } type AccountNameFieldProps = { account?: AccountEntity; accountName: string; isNameEditable: boolean; saveNameError?: ReactNode; onSaveName: (newName: string) => void; }; function AccountNameField({ account, accountName, isNameEditable, saveNameError, onSaveName, }: AccountNameFieldProps) { const { t } = useTranslation(); const [editingName, setEditingName] = useState(false); const handleSave = (newName: string) => { onSaveName(newName); setEditingName(false); }; if (editingName) { return ( <> setEditingName(false)} style={{ fontSize: 25, fontWeight: 500, marginTop: -3, marginBottom: -4, marginLeft: -6, paddingTop: 2, paddingBottom: 2, width: Math.max(20, accountName.length) + 'ch', }} /> {saveNameError && ( {saveNameError} )} ); } else { if (isNameEditable) { return ( {account && account.closed ? t('Closed: {{ accountName }}', { accountName }) : accountName} {account && ( )} ); } else { return ( {account && account.closed ? t('Closed: {{ accountName }}', { accountName }) : accountName} ); } } } type AccountMenuProps = { account: AccountEntity; canSync: boolean; showBalances: boolean; canShowBalances: boolean; showCleared: boolean; showReconciled: boolean; isSorted: boolean; onMenuSelect: ( item: | 'link' | 'unlink' | 'close' | 'reopen' | 'export' | 'toggle-balance' | 'remove-sorting' | 'toggle-cleared' | 'toggle-reconciled', ) => void; }; function AccountMenu({ account, canSync, showBalances, canShowBalances, showCleared, showReconciled, isSorted, onMenuSelect, }: AccountMenuProps) { const { t } = useTranslation(); const syncServerStatus = useSyncServerStatus(); return ( { onMenuSelect(item); }} items={[ ...(isSorted ? [ { name: 'remove-sorting', text: t('Remove all sorting'), } as const, ] : []), ...(canShowBalances ? [ { name: 'toggle-balance', text: showBalances ? t('Hide running balance') : t('Show running balance'), } as const, ] : []), { name: 'toggle-cleared', text: showCleared ? t('Hide “cleared” checkboxes') : t('Show “cleared” checkboxes'), }, { name: 'toggle-reconciled', text: showReconciled ? t('Hide reconciled transactions') : t('Show reconciled transactions'), }, { name: 'export', text: t('Export') }, ...(account && !account.closed ? canSync ? [ { name: 'unlink', text: t('Unlink account'), } as const, ] : syncServerStatus === 'online' ? [ { name: 'link', text: t('Link account'), } as const, ] : [] : []), ...(account.closed ? [{ name: 'reopen', text: t('Reopen account') } as const] : [{ name: 'close', text: t('Close account') } as const]), ]} /> ); }