mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-30 10:14:53 -05:00
228 lines
7.2 KiB
TypeScript
228 lines
7.2 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import type { FormEvent } from 'react';
|
|
import { Form } from 'react-aria-components';
|
|
import { Trans } from 'react-i18next';
|
|
|
|
import { Button } from '@actual-app/components/button';
|
|
import { SvgCheckCircle1 } from '@actual-app/components/icons/v2';
|
|
import { InitialFocus } from '@actual-app/components/initial-focus';
|
|
import { Input } from '@actual-app/components/input';
|
|
import { styles } from '@actual-app/components/styles';
|
|
import { Text } from '@actual-app/components/text';
|
|
import { theme } from '@actual-app/components/theme';
|
|
import { View } from '@actual-app/components/view';
|
|
import { format as formatDate } from 'date-fns';
|
|
import { t } from 'i18next';
|
|
|
|
import type { Query } from 'loot-core/shared/query';
|
|
import { tsToRelativeTime } from 'loot-core/shared/util';
|
|
import type { AccountEntity } from 'loot-core/types/models';
|
|
import type { TransObjectLiteral } from 'loot-core/types/util';
|
|
|
|
import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
|
|
import { useFormat } from '@desktop-client/hooks/useFormat';
|
|
import { useLocale } from '@desktop-client/hooks/useLocale';
|
|
import { useSheetValue } from '@desktop-client/hooks/useSheetValue';
|
|
import * as bindings from '@desktop-client/spreadsheet/bindings';
|
|
|
|
type ReconcilingMessageProps = {
|
|
balanceQuery: { name: `balance-query-${string}`; query: Query };
|
|
targetBalance: number;
|
|
onDone: () => void;
|
|
onCreateTransaction: (targetDiff: number) => void;
|
|
};
|
|
|
|
export function ReconcilingMessage({
|
|
balanceQuery,
|
|
targetBalance,
|
|
onDone,
|
|
onCreateTransaction,
|
|
}: ReconcilingMessageProps) {
|
|
const cleared =
|
|
useSheetValue<'balance', `balance-query-${string}-cleared`>({
|
|
name: (balanceQuery.name +
|
|
'-cleared') as `balance-query-${string}-cleared`,
|
|
value: 0,
|
|
query: balanceQuery.query.filter({ cleared: true }),
|
|
}) ?? 0;
|
|
const format = useFormat();
|
|
const targetDiff = targetBalance - cleared;
|
|
|
|
const clearedBalance = format(cleared, 'financial');
|
|
const bankBalance = format(targetBalance, 'financial');
|
|
const difference =
|
|
(targetDiff > 0 ? '+' : '') + format(targetDiff, 'financial');
|
|
|
|
return (
|
|
<View
|
|
style={{
|
|
flexDirection: 'row',
|
|
alignSelf: 'center',
|
|
backgroundColor: theme.tableBackground,
|
|
...styles.shadow,
|
|
borderRadius: 4,
|
|
marginTop: 5,
|
|
marginBottom: 15,
|
|
padding: 10,
|
|
}}
|
|
>
|
|
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
{targetDiff === 0 ? (
|
|
<View
|
|
style={{
|
|
color: theme.noticeTextLight,
|
|
flex: 1,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
}}
|
|
>
|
|
<SvgCheckCircle1
|
|
style={{
|
|
width: 13,
|
|
height: 13,
|
|
color: 'inherit',
|
|
marginRight: 3,
|
|
}}
|
|
/>
|
|
<Trans>All reconciled!</Trans>
|
|
</View>
|
|
) : (
|
|
<View style={{ color: theme.tableText }}>
|
|
<Text style={{ fontStyle: 'italic', textAlign: 'center' }}>
|
|
<Trans>
|
|
Your cleared balance{' '}
|
|
<strong>{{ clearedBalance } as TransObjectLiteral}</strong>{' '}
|
|
needs <strong>{{ difference } as TransObjectLiteral}</strong> to
|
|
match
|
|
<br /> your bank's balance of{' '}
|
|
<Text style={{ fontWeight: 700 }}>
|
|
{{ bankBalance } as TransObjectLiteral}
|
|
</Text>
|
|
</Trans>
|
|
</Text>
|
|
</View>
|
|
)}
|
|
<View style={{ marginLeft: 15 }}>
|
|
<Button variant="primary" onPress={onDone}>
|
|
{targetDiff === 0
|
|
? t('Lock transactions')
|
|
: t('Exit reconciliation')}
|
|
</Button>
|
|
</View>
|
|
{targetDiff !== 0 && (
|
|
<View style={{ marginLeft: 15 }}>
|
|
<Button onPress={() => onCreateTransaction(targetDiff)}>
|
|
<Trans>Create reconciliation transaction</Trans>
|
|
</Button>
|
|
</View>
|
|
)}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
type ReconcileMenuProps = {
|
|
account: AccountEntity;
|
|
onReconcile: (amount: number | null) => void;
|
|
onClose: () => void;
|
|
};
|
|
|
|
export function ReconcileMenu({
|
|
account,
|
|
onReconcile,
|
|
onClose,
|
|
}: ReconcileMenuProps) {
|
|
const balanceQuery = bindings.accountBalance(account.id);
|
|
const clearedBalance = useSheetValue<'account', `balance-${string}-cleared`>({
|
|
name: (balanceQuery.name + '-cleared') as `balance-${string}-cleared`,
|
|
value: null,
|
|
query: balanceQuery.query.filter({ cleared: true }),
|
|
});
|
|
const lastSyncedBalance = account.balance_current;
|
|
const format = useFormat();
|
|
const dateFormat = useDateFormat() || 'MM/dd/yyyy';
|
|
const locale = useLocale();
|
|
|
|
const [inputValue, setInputValue] = useState<string | null>();
|
|
// useEffect is needed here. clearedBalance does not work as a default value for inputValue and
|
|
// to use a button to update inputValue we can't use defaultValue in the input form below
|
|
useEffect(() => {
|
|
if (clearedBalance != null) {
|
|
setInputValue(format(clearedBalance, 'financial'));
|
|
}
|
|
}, [clearedBalance, format]);
|
|
|
|
function onSubmit(e: FormEvent<HTMLFormElement>) {
|
|
e.preventDefault();
|
|
|
|
if (inputValue === '') {
|
|
return;
|
|
}
|
|
|
|
const amount =
|
|
inputValue != null
|
|
? format.fromEdit(inputValue, clearedBalance)
|
|
: clearedBalance;
|
|
|
|
onReconcile(amount);
|
|
onClose();
|
|
}
|
|
|
|
return (
|
|
<Form onSubmit={onSubmit}>
|
|
<View style={{ padding: '5px 8px' }}>
|
|
<Text>
|
|
<Trans>
|
|
Enter the current balance of your bank account that you want to
|
|
reconcile with:
|
|
</Trans>
|
|
</Text>
|
|
{inputValue != null && (
|
|
<InitialFocus>
|
|
<Input
|
|
value={inputValue}
|
|
onChangeValue={setInputValue}
|
|
style={{ margin: '7px 0' }}
|
|
/>
|
|
</InitialFocus>
|
|
)}
|
|
{lastSyncedBalance != null && (
|
|
<View>
|
|
<Text>
|
|
<Trans>Last Balance from Bank: </Trans>
|
|
{format(lastSyncedBalance, 'financial')}
|
|
</Text>
|
|
<Button
|
|
onPress={() =>
|
|
setInputValue(format(lastSyncedBalance, 'financial'))
|
|
}
|
|
style={{ marginBottom: 7 }}
|
|
>
|
|
<Trans>Use last synced total</Trans>
|
|
</Button>
|
|
</View>
|
|
)}
|
|
<Text style={{ color: theme.pageTextSubdued, paddingBottom: 6 }}>
|
|
{account?.last_reconciled
|
|
? t('Reconciled {{ relativeTimeAgo }} ({{ absoluteDate }})', {
|
|
relativeTimeAgo: tsToRelativeTime(
|
|
account.last_reconciled,
|
|
locale,
|
|
),
|
|
absoluteDate: formatDate(
|
|
new Date(parseInt(account.last_reconciled ?? '0', 10)),
|
|
dateFormat,
|
|
{ locale },
|
|
),
|
|
})
|
|
: t('Not yet reconciled')}
|
|
</Text>
|
|
<Button type="submit" variant="primary">
|
|
<Trans>Reconcile</Trans>
|
|
</Button>
|
|
</View>
|
|
</Form>
|
|
);
|
|
}
|